@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
package/README.md CHANGED
@@ -1,3 +1,1869 @@
1
- # Temporary Holding Version
1
+ # @scaffold-ui-kit/preview
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A preview for kits built with [scaffold-ui-kit](https://github.com/djurnamn/scaffold-ui-kit).
4
+ It renders every component of a kit side by side in React, Vue and Svelte,
5
+ each in its own iframe running that framework natively, and as the markup of
6
+ its html target where the kit builds one, with controls for the
7
+ component's props, named scenarios, and an editor for composing its slot
8
+ content. Everything kit-specific comes from the kit's configuration, so the
9
+ same package previews any kit.
10
+
11
+ ```sh
12
+ npm install --save-dev @scaffold-ui-kit/preview charcell @zag-js/react
13
+ npx scaffold-ui-kit-preview dev
14
+ ```
15
+
16
+ `charcell` and `@zag-js/react` are peers of the charcell shell, the shell the
17
+ preview ships with (below); a kit using another shell leaves them out. A kit
18
+ made with `scaffold-ui-kit initialize` has all of this set up.
19
+
20
+ ## What you get
21
+
22
+ - **One pane per target.** A component renders in each target it is built
23
+ for, next to the others, so a difference between targets is visible at a
24
+ glance. A toggle per target hides the ones you don't need. The targets
25
+ are the kit's own `targets`: a kit built for React and Vue gets those two
26
+ panes and no Svelte one. A story that throws as it renders shows
27
+ `render error:` and the error's message in its pane, in each target.
28
+ - **An html pane.** A kit whose `targets` include `html` gets one more pane
29
+ and toggle, after the other targets': the markup the story renders to, under
30
+ the kit's theme, options and `mount` frame, with no JavaScript.
31
+ The markup shows under the styles the kit's build wrote for the html
32
+ target, for the story's component and each component it composes: the
33
+ `<style>` blocks of each page in `dist/html` where the kit's
34
+ `stylesheetPlacement` is `in-file`, or each component's stylesheet there
35
+ where it is `separate-file`, an SCSS one
36
+ compiled with the kit's own `sass`. The markup follows the templates as
37
+ they are saved, the page reloading, and the styles follow the last
38
+ `npm run build`.
39
+ It follows the scenario and every prop and editor change. It runs none of
40
+ the story's behavior: html writes a declared event as a comment, and the
41
+ pane takes out anything in the markup that would run. What the markup
42
+ cannot evaluate at rest, such as a part shown on a condition the
43
+ component's script holds, is left out, and the pane's legend says how many
44
+ warnings that gives, the count opening them in a popover. The expressions one component cannot evaluate for one reason, such
45
+ as several reading a per-instance id its script generates, count as one
46
+ warning.
47
+ - **Pane widths.** The panes fit the space beside each other, or every one
48
+ is laid out at a width you pick, such as `large` 1280 pixels, so a
49
+ component that responds to its width shows as it does on a screen that
50
+ wide; the default shell stacks the panes as rows when the width needs the
51
+ room. A kit can name its own widths.
52
+ - **Controls from the template.** Each declared prop gets a control: a select
53
+ for a union of string literals, a toggle for a boolean, a text or number
54
+ input otherwise. A union of booleans and string literals, such as a
55
+ checkbox's `boolean | 'indeterminate'`, gets a select of `true`, `false` and
56
+ the literals, and the link keeps the choice as the value it is. A union that
57
+ also takes any string (`string & {}`) or a structured form, such as a list
58
+ of breakpoint values, still gets a select of its literals, and a type the
59
+ template imports by name reads as its members. Edits reach every pane at
60
+ once without a reload.
61
+ - **Two-way props.** A prop whose template declares a `bindable` object,
62
+ such as a toggle's `pressed`, keeps its own value while a story leaves it
63
+ out, in every pane, however the story's other props change. A story that
64
+ gives it is controlled, as a consumer's state would be:
65
+ each value the component reports through its change callback becomes the
66
+ story's prop, so a click in any pane changes every pane, the html, the
67
+ controls and the link alike. A pane takes the value at once, as a
68
+ consumer's state does, so typing mid-text in a controlled text field
69
+ keeps the caret where it is. Its code binds the prop as a consumer does,
70
+ so copied code responds like the story: a controlled `Toggle` prints
71
+ `const [pressed, setPressed] = useState(true);` and `<Toggle
72
+ pressed={pressed} onPressedChange={setPressed}>` in React, `v-model:pressed`
73
+ on a `ref` in Vue and `bind:pressed` in Svelte, each starting at the
74
+ story's value (see A story's source). The static build is the exception
75
+ for the html: it prints the html of each scenario alone, so once a click
76
+ writes back a value other than the scenario's, the html pane says it has
77
+ none until the value is the scenario's again.
78
+ - **Scenarios.** Named prop sets a kit curates per component, plus one
79
+ generated per enum value and per flipped boolean, and an `Empty` scenario
80
+ for a component that takes content.
81
+ - **A scenario editor.** A component's slot content as an editable tree:
82
+ add components, plain HTML tags and text, reorder them by dragging, edit
83
+ each node's props. Every edit regenerates the story and reloads the panes,
84
+ so what you compose renders as the kit's real components.
85
+ - **A story's code.** What a consumer writes to get the story on screen, in
86
+ React, Vue and Svelte, and the markup it renders to as html, following
87
+ every prop edit, scenario pick and editor change.
88
+ - **Kit options and themes.** Toggles and selects the kit declares (a color
89
+ mode, a density, a frame around the story), and the kit's compiled theme
90
+ stylesheets.
91
+ - **Custom views.** Panels the kit writes itself, such as a tokens page or
92
+ an icon gallery, listed beside the components.
93
+ - **A shell of your choosing.** The interface around the panes is a shell
94
+ the kit names. The charcell shell ships with the preview, with slots for a
95
+ kit's own content; a shell can also be written from scratch, in React, Vue,
96
+ Svelte or plain DOM, on the preview's shell contract.
97
+
98
+ ## Setting it up
99
+
100
+ The preview reads the `preview` block of the kit configuration,
101
+ `scaffold-ui-kit.ts` (or `scaffold-ui-kit.json`) in the kit root. The build
102
+ leaves the block alone; this package owns and validates it. A key the block
103
+ does not take, or one an object inside it does not (a source, `codeBlock`,
104
+ `setupModules`, `landing`, a hand-written component's mapping), stops
105
+ `check`, `build` and `dev`, named with the keys allowed there, as in `unknown key
106
+ 'preview.persistance'; the 'preview' block takes shell, sources, ...`, or with
107
+ what to use instead where one key is meant.
108
+
109
+ <!-- example: preview-configuration -->
110
+ ```ts
111
+ /// <reference types="@scaffold-ui-kit/preview" />
112
+ import { defineKit } from 'scaffold-ui-kit';
113
+
114
+ export default defineKit({
115
+ name: 'my-ui-kit',
116
+ targets: ['react', 'vue', 'svelte'],
117
+ styling: [],
118
+ preview: {
119
+ shell: '@scaffold-ui-kit/preview/shells/charcell',
120
+ sources: [
121
+ {
122
+ id: 'core',
123
+ label: 'Components',
124
+ order: 0,
125
+ moduleBase: 'my-ui-kit',
126
+ templatesDirectory: 'src/components',
127
+ storiesDirectory: 'preview/stories',
128
+ },
129
+ ],
130
+ themes: [
131
+ { id: 'default', label: 'Default', css: 'dist/themes/default.css', assets: 'dist/fonts' },
132
+ ],
133
+ canvas: { namePrefix: 'my-ui-kit' },
134
+ options: [
135
+ {
136
+ id: 'theme',
137
+ label: 'theme',
138
+ values: [{ value: 'default', label: 'Default' }],
139
+ default: 'default',
140
+ group: 'kit',
141
+ scope: 'global',
142
+ apply: { type: 'stylesheet' },
143
+ },
144
+ {
145
+ id: 'mode',
146
+ label: 'mode',
147
+ values: [
148
+ { value: 'light', label: 'light' },
149
+ { value: 'dark', label: 'dark' },
150
+ ],
151
+ default: 'light',
152
+ scope: 'global',
153
+ apply: { type: 'root-attribute', attribute: 'data-mode' },
154
+ },
155
+ {
156
+ id: 'outline',
157
+ label: 'outline',
158
+ default: false,
159
+ group: 'canvas',
160
+ scope: 'global',
161
+ apply: { type: 'class', target: 'body', className: 'is-outlined' },
162
+ },
163
+ ],
164
+ },
165
+ });
166
+ ```
167
+
168
+ The first line loads this package's types for the block, so an editor
169
+ checks it as it checks the rest of the configuration.
170
+
171
+ Every command finds the kit configuration the same way: the file
172
+ `--configuration <path>` names, relative to the directory the command runs
173
+ in, else the one the kit's `package.json` names
174
+ as `"scaffold-ui-kit": { "configuration": "<path>" }`, relative to the kit
175
+ root, else `scaffold-ui-kit.ts` or `scaffold-ui-kit.json` in the kit root.
176
+ Both in the root is an error.
177
+
178
+ The keys the preview's pages read, `options`, `canvas`, `themes`, `mount` and
179
+ `viewCanvas` (see The kit's vocabulary), reach the browser as JSON, so they
180
+ hold plain data: a function, a regular expression or `undefined` in a list
181
+ there stops `check`, `build` and `dev`, naming where it sits, as in
182
+ `preview.options[0].apply.className is a function, which can't be sent to
183
+ the browser as JSON`. The kit configuration itself never reaches the
184
+ browser, so `scaffold-ui-kit.ts` can import what it computes these from,
185
+ such as the kit's tokens. `controlHints` and `labels` are read in Node
186
+ alone, so in `scaffold-ui-kit.ts` their hooks can be functions; a kit
187
+ configured in `scaffold-ui-kit.json` leaves those hooks out.
188
+
189
+ - `sources` - the packages whose components the preview shows, each under its
190
+ own navigation section. A source's components import from
191
+ `<moduleBase>/<target>`, so the preview renders the kit as a consumer gets
192
+ it. The kit is the first source; another package's components can be
193
+ another. For the kit's own source that means importing the kit by its
194
+ package name, so its `package.json` needs an `exports` entry per target (`"./react":
195
+ "./dist/react/index.ts"`, and likewise for `vue` and `svelte`). A
196
+ `moduleBase` containing `{target}` names the module itself, with the
197
+ target in its place: a kit built into one package per target (its
198
+ configuration's `packages`) sets it to the pattern its target packages
199
+ share, such as `'@acme/{target}'` for `@acme/react` and `@acme/vue`. Each
200
+ of those packages, and the styles package, has to resolve from the kit
201
+ root, as a workspace package or a dev dependency of the kit root.
202
+ - `templatesDirectory` - the component templates to read: TypeScript,
203
+ JSON and JavaScript ones (`.ts`, `.json`, `.js`, `.mjs`, `.cjs`) alike,
204
+ as the kit's build reads them.
205
+ - `storiesDirectory` - the story files that curate each component's story
206
+ (below).
207
+ - `manualModule` - a module whose default export returns the components
208
+ that have no template, such as a hand-written driver, which gets its
209
+ story this way, as `() => ManualComponentSpecification[]`. Each gives its
210
+ `name`, the `file` it lives in (informational), and `targets`
211
+ when it renders in only some, and gives its props inline in `props`, or
212
+ names its React props type in
213
+ `propsFrom: '<file>#<TypeName>'`, a file relative to the kit root. The
214
+ preview reads that type's members from source, following imports and
215
+ intersections. A type built on a template component reads too:
216
+ `ComponentProps<typeof ButtonVisual>` takes the template's props, through
217
+ an `Omit<..., 'name'>` and a generic helper such as
218
+ `Props extends unknown ? Omit<Props, 'onChange'> : never`. A member whose
219
+ type is not a control is kept as a hidden prop.
220
+ - `options`, `canvas`, `themes`, `mount`, `viewCanvas`, `paneWidths`,
221
+ `controlHints` and `labels` - the kit's vocabulary (below).
222
+ - `shell` - the shell that renders the preview's interface (below). Required:
223
+ a package specifier, resolved from the kit as its own imports are, or a
224
+ module path relative to the kit root, whose default export is a shell.
225
+ `@scaffold-ui-kit/preview/shells/charcell` is the shell shipped with the
226
+ preview; a kit that names it installs `charcell` and `@zag-js/react`. The
227
+ long form, `{ module, page }`, names the shell's page module beside it:
228
+ what the host page shows before the shell has loaded (below).
229
+ - `viewsModule` - the kit's custom views (below).
230
+ - `codeBlock` - the kit's own code block for the code view, as
231
+ `{ module, framework }` (below).
232
+ - `setupModules` - per target, a module each preview iframe imports once for
233
+ its side effects, such as mounting a toast container a story assumes.
234
+ - `htmlStylesheets` - stylesheets the html pane loads beside the theme's and
235
+ its components' own, such as a preflight a framework's setup module would
236
+ import. The html pane runs no script, so this is its counterpart to a
237
+ setup module.
238
+ - `aliases` - import aliases for the preview only.
239
+ - `scssLoadPaths` - sass load paths for per-component SCSS.
240
+ - `optimizeDepsExclude` - packages Vite should not prebundle, such as a
241
+ dependency that ships raw `.svelte` files.
242
+ - `dedupe` - packages the preview loads one copy of, beside React, Vue and
243
+ Svelte, which it always loads once. Name a package the shell and the kit's
244
+ components both import, when the two must share its instances: a library
245
+ with a plugin registry or other module-level state fails when the shell's
246
+ copy meets the kit's. Every import of a listed package, and of its
247
+ subpaths, resolves to the copy the kit root resolves, prebundled once, so
248
+ each must be installed where the kit root finds it; one that is not is an
249
+ error. List the packages whose instances meet, such as a library's core
250
+ and the packages around it that hold state, and keep them out of
251
+ `optimizeDepsExclude`. After changing the list, clear the kit's
252
+ `.preview-generated/.vite` directory, Vite's dependency cache.
253
+ - `omitPattern` - a regular expression, as a string, matched against each
254
+ component's name; a match gets no story, as a story file's `omit` does. For a
255
+ kit whose naming marks a compound's parts, such as `"(Driver|Visual)$"`.
256
+ Unset, no component is left out by name.
257
+ - `handWrittenComponents` - what each component written by hand renders as
258
+ in a story's `html`: `"content"` for one that renders only the content
259
+ passed into it, such as a driver that adds behavior to markup it is given,
260
+ or the name of the template it renders, which then takes its props and
261
+ content (its own props the template does not declare are dropped). Where
262
+ the component puts content in a different slot of that template, give
263
+ `{ "template": "FieldVisual", "slots": { "default": "inner" } }`. Either
264
+ way one comment around it says its behavior is not included.
265
+ - `port` - the dev server's port. Default `6100`; `dev --port` overrides it,
266
+ and a taken port moves to the next free one.
267
+ - `host` - the addresses the dev server listens on: an address such as
268
+ `"0.0.0.0"`, or `true` for every address, so another device on the network
269
+ can open the preview. Default: this machine only; `dev --host` overrides
270
+ it.
271
+ - `persistence` - where the preview remembers what a person chose: `"url"`
272
+ (the default) or `"url-and-local-storage"` (below).
273
+ - `landing` - the entry the preview opens on when its address names no
274
+ story or view: `{ "view": "foundations" }` for a custom view, or
275
+ `{ "component": "components-button", "scenario": "ghost" }` for a story,
276
+ on one of its scenarios or, without `scenario`, on the scenario it starts
277
+ on. The ids are the ones the address carries (below). `check` and
278
+ `generate` fail on a story or scenario the kit has none of; a view the
279
+ views module does not register opens the first story instead, with a
280
+ warning in the browser's console. The `empty` scenario renders through
281
+ the development server alone, so the static build opens that story on
282
+ the scenario it starts on, with a warning in the console. Default: the
283
+ first story the navigation lists.
284
+
285
+ Paths are relative to the kit root, and the kit's modules are TypeScript the
286
+ preview compiles as it loads them.
287
+
288
+ ## Story files
289
+
290
+ A component's story file, `<Name>.story.ts` in its source's
291
+ `storiesDirectory`, curates the story the preview shows for it: the
292
+ props it starts from, its controls, its slot content and its scenarios. The
293
+ file default-exports a `Story` object. Every field is optional, and a
294
+ component without a story file still gets a story from its template.
295
+
296
+ <!-- example: preview-story -->
297
+ ```ts
298
+ import type { Story } from '@scaffold-ui-kit/preview/types';
299
+
300
+ const story: Story = {
301
+ category: 'Inputs',
302
+ defaultProps: { label: 'Save' },
303
+ controls: { size: { number: { min: 1, max: 4, step: 1 } }, onClick: false },
304
+ slots: { default: { kind: 'text', value: 'Save changes' } },
305
+ scenarios: [{ id: 'destructive', label: 'Destructive', props: { variant: 'danger' } }],
306
+ };
307
+ export default story;
308
+ ```
309
+
310
+ - `defaultProps` - the props the story starts from. Beside the declared
311
+ props, a component that forwards native attributes (a `passthrough`
312
+ element, or a `rest` it spreads) takes HTML attributes such as
313
+ `aria-label` and `class` here and in scenarios. A story spells them as
314
+ HTML does, the form Vue and Svelte take. React takes them as React
315
+ spells them, in the React pane and in the React code: `class` as
316
+ `className` (joined with a `className` the story also gives), a `style`
317
+ string as a style object (without `!important`, which a style object
318
+ cannot hold), and the attributes React
319
+ renames by their React names (`tabindex` as `tabIndex`, `for` as
320
+ `htmlFor`, `readonly` as `readOnly`). A component that declares a prop by
321
+ the exact name takes it as written. The same goes for an `element` fill's
322
+ `props` and an `html` fill's (see Slot content, below).
323
+ - `controls` - per prop, tweaks to its control, or `false` to leave the prop
324
+ out of the controls and the generated scenarios.
325
+ - `fixed` - props always passed with the given value and never shown as
326
+ controls. Like `defaultProps`, it takes `children` and the native
327
+ attributes a component forwards.
328
+ - `generate` - which props get generated scenarios: `false` for none, a list
329
+ of prop names for only those, or `{ exclude: [...] }` for all but those.
330
+ - `slots` - the story's slot content: `default` for the default slot and
331
+ `named` for the others, each a fill (see Slot content, below). Without
332
+ `slots` or a `children` text, the panes show the component's name in its
333
+ default slot, so the slot is visible; the story's code leaves it empty.
334
+ - `around` - content the story renders beside the component, as its
335
+ siblings: `before` it and `after` it (see Content beside the component,
336
+ below).
337
+ - `scenarios` - named prop sets, one of which may be marked `default`. A
338
+ scenario's `slots` give it slot content of its own, in the shape of the
339
+ story's `slots`: a slot it fills shows the scenario's fill in place of the
340
+ story's, and every other slot keeps the story's, as in
341
+ `{ id: 'failed', props: { tone: 'loud' }, slots: { default: { kind: 'text', value: 'Could not save.' } } }`.
342
+ The panes, each target's code, the html, the editor and a static build
343
+ all show that content when the scenario is picked. A
344
+ scenario that takes a generated scenario's id (`variant-solid` for
345
+ `variant: 'solid'`, or `disabled` for a boolean) replaces it, so it should
346
+ set that value. `empty` is the preview's own Empty scenario's id, which no
347
+ curated scenario can take; a boolean named `empty` gets `empty-true`
348
+ instead; one that sets a value alone stands in for its generated
349
+ scenario under its own id. A scenario's `description` says what it shows,
350
+ for a shell to show beside it; markdown is allowed. The component's own
351
+ description is the template's `description`. A scenario's `options` set
352
+ the kit's options when it is picked, so it can show the component in a
353
+ setting: `{ id: 'on-dark', options: { mode: 'dark', surface: '3' } }`.
354
+ They are set as if chosen by hand: a global value stays when another
355
+ scenario is picked, unless that one sets it too, and a story value lasts
356
+ until another story is selected. A scenario unsets a `defaultProps` value
357
+ with `null`, not `undefined`: `undefined` doesn't reach the panes, which
358
+ keep the `defaultProps` value, and `check` warns of a scenario prop set to
359
+ `undefined` that such a value covers (an error under `--strict`). Every
360
+ pane leaves a prop given `null` out, as the story's code does, so the
361
+ component's declared default applies. A two-way prop is the exception:
362
+ given `null`, it is still controlled, at `null`, and the code binds state
363
+ starting at `null`;
364
+ `check` reports one whose declared type does not take `null`, as that
365
+ code would not type-check. A scenario that controls a two-way prop need
366
+ not unset its default prop (a `defaultValue` beside `value`): the code
367
+ leaves it out once the story gives the prop. An empty string is a value
368
+ like any other: a prop given `''`, by a story, a control or a component's
369
+ own change, reaches every pane as `''`.
370
+ - `options` - per-story values for the kit's story-scoped options, or `false`
371
+ to make an option unavailable for this component.
372
+ - `omit` - leave the component out (a part only its parent renders); a kit
373
+ can leave out a whole naming pattern with `omitPattern`.
374
+ - `standalone` - whether the component has a story of its own. Default
375
+ `true`. `standalone: false` keeps the component out of the navigation but
376
+ offers it in the editor: a compound's subcomponents, a shared building
377
+ block, a component only a fixture needs. A component its compound's root
378
+ lists in `parts` - in the root's template, or in the kit configuration's
379
+ `parts` for a root with no template - is `standalone: false` without the
380
+ flag. `check`
381
+ warns of `standalone: false` on a component no root lists, since its
382
+ compound is not copied whole either, unless the component is no one compound's part: two or
383
+ more templates render it (each template counts, a component's omitted
384
+ counterparts included), a story of its source labeled with its name stands
385
+ in for it, or it lists parts of its own.
386
+ - `unlisted` - keep the story out of what a reader browses: the
387
+ navigation, the editor's component picker, the component count and the
388
+ catalog a custom view reads. It is generated and renders in every
389
+ target as any story does, and opens at its address
390
+ (`?component=<id>`) and through the testing helpers, so it suits a story
391
+ that exists for a kit's own browser tests. A component written by hand
392
+ takes `unlisted: true` in its manual spec. An omitted component or one
393
+ that is `standalone: false` has no story to keep out, so `check` reports
394
+ `unlisted` beside `omit` or `standalone: false` as an error.
395
+ - `label` - the name shown in the navigation, when it should differ. A story
396
+ labeled with the name of a component in its source that is
397
+ `standalone: false` is that component's story.
398
+
399
+ ### Slot content
400
+
401
+ A fill is one node of slot content, its `kind` naming what it is. This
402
+ story file fills a `DataList`'s default slot with text, its `header` slot with a
403
+ kit component that has a named slot of its own, and its scoped `row` slot,
404
+ which exposes each `row`:
405
+
406
+ <!-- example: preview-slot-content -->
407
+ ```ts
408
+ const story: Story = {
409
+ defaultProps: { rows: [{ id: '1', name: 'Ada' }, { id: '2', name: 'Grace' }] },
410
+ slots: {
411
+ default: { kind: 'text', value: 'Two people' },
412
+ named: {
413
+ header: {
414
+ kind: 'element',
415
+ component: 'IconLabel',
416
+ props: { size: 'small' },
417
+ slots: {
418
+ icon: { kind: 'html', tag: 'span', props: { 'aria-hidden': 'true' }, children: { kind: 'text', value: '#' } },
419
+ },
420
+ children: { kind: 'text', value: 'People' },
421
+ },
422
+ row: {
423
+ kind: 'scoped',
424
+ slot: { react: 'row', vue: 'row', svelte: 'row' },
425
+ parameters: ['row'],
426
+ react: '<strong>{row.name}</strong>',
427
+ vue: '<strong>{{ row.name }}</strong>',
428
+ svelte: '<strong>{row.name}</strong>',
429
+ html: [{ type: 'element', tag: 'strong', children: [{ type: 'text', expression: 'row.name' }] }],
430
+ },
431
+ },
432
+ },
433
+ };
434
+ ```
435
+
436
+ | `kind` | Fields | What it fills |
437
+ |---|---|---|
438
+ | `text` | `value` | Text. |
439
+ | `element` | `component`, `props`, `children`, `slots` | A kit component. `children` is one fill for its default slot, and `slots` maps its named slots to fills; a `default` key there is an error. |
440
+ | `html` | `tag`, `props`, `children` | A plain HTML element, with one fill as its content. |
441
+ | `fragment` | `children` | A list of fills, side by side. |
442
+ | `raw` | `react`, `vue`, `svelte`, `html`, `imports` | Source written per target: a JSX expression for React, markup for Vue and Svelte. `imports` names the kit components the source uses, as tags or as values (`:as="Button"` in Vue), as a module's imports are. |
443
+ | `scoped` | `slot`, `parameters`, `react`, `vue`, `svelte`, `reactScope`, `html`, `imports` | A slot that exposes values, as a top-level fill in `default` or `named` only. `slot` names the slot in each target, since a render prop, a Vue slot and a Svelte snippet may be named differently. `parameters` are the exposed values each body reads. `reactScope: 'positional'` passes them to React as separate arguments, for a hand-written component whose render prop takes them that way. A React body that is not one expression, two or more sibling roots or text such as `{item}` or `remove <Button />`, is returned in a fragment, `<>...</>`; a body opening with a word reads as an expression unless two words or a word and a placeholder or tag stand side by side. Vue and Svelte take the body as it is. A slot whose template sets `scopeRequired` takes this fill alone: the check refuses any other there, and the editor neither offers nor takes a drop of plain content for it. |
444
+ | `part` | `name`, `parameters`, `parameterTypes`, `props`, `react`, `vue`, `svelte`, `html`, `imports` | A call to content the story declares once by `name` (below). Its React body goes in a fragment as a `scoped` fill's does. |
445
+
446
+ `props`, `children` (of an `element` or `html` fill), `slots`, `html`,
447
+ `imports`, `reactScope` and `parameterTypes` are optional. `raw`,
448
+ `scoped` and `part` fills take an `html` for the story's `html` source: the
449
+ markup the fill renders to at rest, as a string (not for `part`) or as
450
+ template nodes, which render as the kit's templates do and may compose the
451
+ kit's components. A node composes a component by naming it as its `tag`,
452
+ with the component's props as `attributes`:
453
+ `{ type: 'element', tag: 'Button', attributes: { variant: 'outline' }, children: [{ type: 'text', content: 'Cancel' }] }`
454
+ renders the kit's outline Button. A `scoped` fill's nodes read the slot's
455
+ values under its `parameters`. Without an `html`, the story has no `html`
456
+ source.
457
+
458
+ A `part` fill calls content the story declares once by `name`, with the
459
+ `props` it passes; its body per target reads them by the names in
460
+ `parameters` and may call the part again, so a tree or lists nested in each
461
+ other's items render as deep as their data goes. React declares a part as a
462
+ function component, Vue as a component (in the story's source, a single-file
463
+ component of its own, printed after the story's), and Svelte as a snippet at
464
+ the top of the markup. A body calls it as `<Name ... />` in React and Vue and
465
+ `{@render Name({ ... })}` in Svelte. Its `html` is template nodes that read
466
+ `parameters`, where a `Name` element renders the part again; every call of
467
+ one name carries the same declaration. Its `parameterTypes` give each
468
+ parameter a TypeScript type for the printed source: React's props and
469
+ Svelte's snippet are typed with them (`any` where a parameter has none), and
470
+ Vue's `defineProps` once any is given.
471
+
472
+ A scenario sets `id`, `label`, `description`, `props`, `options` and
473
+ `default`, and nothing else: slot content belongs to the story file, shared by
474
+ its scenarios.
475
+
476
+ ### Content beside the component
477
+
478
+ `around` renders content beside the component rather than inside it, such
479
+ as a hint whose id the component's `aria-describedby` names. `before` and
480
+ `after` each take a fill of any kind but `scoped`, which needs a slot to
481
+ give it values:
482
+
483
+ <!-- example: preview-around -->
484
+ ```ts
485
+ const story: Story = {
486
+ defaultProps: { label: 'Save', 'aria-describedby': 'save-hint' },
487
+ around: {
488
+ after: {
489
+ kind: 'html',
490
+ tag: 'p',
491
+ props: { id: 'save-hint' },
492
+ children: { kind: 'text', value: 'Saves a draft you can return to.' },
493
+ },
494
+ },
495
+ };
496
+ ```
497
+
498
+ Every pane renders it as the component's siblings, inside the
499
+ `mount` node the component mounts in, so the kit's canvas CSS lays
500
+ them out. The story's code prints them the same way: React returns them in
501
+ a fragment with the component, and Vue's template and Svelte's markup list
502
+ them beside it. The html pane and the html source render them beside the
503
+ component's markup, from the fill's html as slot content's is. The scenario
504
+ editor shows them as locked rows before and after its root, which no edit
505
+ changes. Like `slots`, `around` belongs to the story file, so every scenario of
506
+ the story shows it. `check` reports a scoped fill there, a key other than
507
+ `before` and `after`, and the mistakes it reports in slot content.
508
+
509
+ ### Checking story files
510
+
511
+ `scaffold-ui-kit-preview check` compares the story files with the components.
512
+ A key a story file does not take is an error, named with the keys it does or
513
+ with what to use instead. A prop, slot or value a story file names that the
514
+ component lacks is an error,
515
+ and so is a prop or named slot a fill gives a component it composes, at
516
+ every depth of the slot content. A value outside a prop's choices is
517
+ reported with the choices, adding "any string" or "a structured value"
518
+ where the prop's type takes them beside its literals.
519
+ A scenario's option value that no kit option takes, or that its option
520
+ cannot hold or the story file makes unavailable, is an error too, as is an
521
+ `html` fill giving a void element (`img`, `input`, `br`) children, and a
522
+ target the kit builds, or a `specifier` an `add.shared` entry
523
+ still names for the kit's own package, that its `package.json` has no
524
+ `exports` entry for. A
525
+ prop spelled as React spells an attribute (`autoComplete`) is reported
526
+ with the HTML spelling a story gives it by (`autocomplete`). A `landing`
527
+ entry naming a story the kit has none of, or a scenario its story lacks, is
528
+ an error. An enum value or boolean that no scenario sets is a warning, and
529
+ an error under `--strict`. Props the preview cannot give a control are
530
+ listed, and so are the props `generate` leaves out.
531
+
532
+ The check also reports what `scaffold-ui-kit build` reports for each
533
+ template in the kit's `src/components/`, whichever sources the preview reads,
534
+ without writing any output. It asks the scaffolder the kit has installed,
535
+ or the preview's own when the kit has none (with a warning when the kit's
536
+ copy cannot be loaded or is too old to report), and a kit with no
537
+ `src/components/` gets nothing from it. Each line names the template's path,
538
+ as the build prints it: a template the build refuses, such as one with a
539
+ Tailwind class outside the conversion subset, is an error, and each build
540
+ warning, such as two nodes giving one class different styles, is a warning,
541
+ which `--strict` leaves a warning. A component the kit cannot build doesn't
542
+ pass the check. It covers validating and rendering the templates; a driver
543
+ colliding with a generated file and the stylesheets compiled under
544
+ `stylesheetLink: 'both'` are reported by `scaffold-ui-kit build` alone.
545
+
546
+ ## The kit's vocabulary
547
+
548
+ The preview holds no vocabulary of its own: the kit's themes, options and
549
+ the frame around each story are keys of the `preview` block, as in the
550
+ example above.
551
+
552
+ - `themes` - the compiled theme stylesheets, and optionally a
553
+ directory of assets (fonts) to serve beside each.
554
+ - `canvas` - the prefix for the attributes and classes the preview sets in
555
+ each iframe, an optional page background, and extra CSS. Without it the
556
+ prefix is the kit's `name`, each character other than a letter, a digit,
557
+ `-` or `_` made `-`.
558
+ - `options` - the kit's options. An option says what it means, and
559
+ the preview's interface decides how to show it:
560
+ - Its kind follows from its fields: with `values` it is a choice among
561
+ them, its `default` one of them; without, it is a boolean with a boolean
562
+ `default`. The charcell shell shows a select for a choice and a toggle
563
+ for a boolean; another shell picks its own. `optionKind(option)` gives
564
+ the kind, and `generate` and `check`
565
+ fail on an option that is neither.
566
+ - A `global` option has one value for the session; a `story` option is set
567
+ per story, seeded from the story file, and lasts until another story is
568
+ selected.
569
+ - `group` is a placement hint for the shell: `context` (the default) for
570
+ the setting a component is shown in, such as its color mode, a surface
571
+ or the padding around it; `kit` for the kit's own settings, such as its
572
+ theme; or a name of the kit's choosing, such as `canvas`, for a set that
573
+ belongs together. Where each group shows is the shell's decision; the
574
+ charcell shell's placement, and how a kit changes it, is described
575
+ below.
576
+
577
+ Each declares in `apply` how its value reaches the iframe:
578
+
579
+ | `type` | Fields | Effect |
580
+ |---|---|---|
581
+ | `root-attribute` | `attribute`, `target`, `omitAtDefault` | Sets `attribute` to the value. With `omitAtDefault: true`, the option's default value leaves the attribute off, for a kit whose default mode renders with no attribute. |
582
+ | `class` | `className`, `target` | Toggles `className` by the value's truth; without `className`, sets the value as a `data-<namePrefix>-preview-class` attribute for the kit's CSS to read. |
583
+ | `css-var` | `var`, `target` | Sets the custom property `var`, named in full (`'--surface'`), to the value. |
584
+ | `stylesheet` | none | Switches to the `themes` entry whose `id` is the value. |
585
+
586
+ `false` or an empty value removes the attribute or property. `target` is
587
+ optional: `'root'` for the iframe's `<html>` (the default), `'body'`, or
588
+ `{ ref }` for a node of `mount`, as the `outline` option above names
589
+ `'body'`. A `class` apply also takes `on: 'root' | 'body'`, naming the
590
+ same two elements; `target` wins where both are given.
591
+
592
+ `check`, `generate`, `build` and `dev` report a key an option, one of its
593
+ `values` or an `apply` of these four types does not take, such as a
594
+ `description` where an option takes `title`, naming the keys it takes.
595
+ - `paneWidths` - the widths a shell offers to lay a story's panes
596
+ out at, as a list of `{ label: 'small', width: 375 }`, each width a whole
597
+ number of CSS pixels up to 7680, so the kit can derive them from its own
598
+ breakpoints. Default: `small` 375, `medium` 768 and `large` 1280. No
599
+ label or width appears twice. The list replaces the default, so name
600
+ every width to offer; an empty list offers none. A shell lists them
601
+ narrowest first. `check` and `generate` report a list that breaks these
602
+ rules, naming the key.
603
+ - `mount` - an optional tree of elements wrapped around every story,
604
+ such as a frame that pads it or paints a surface, each node addressable by
605
+ a `ref`.
606
+ - `viewCanvas` - the page background and extra CSS for custom views, which
607
+ have no `mount` frame. Without it a view gets `canvas.pageBackground` and
608
+ nothing else. An option targeting a `mount` node by `ref` does nothing in
609
+ a view.
610
+ - `controlHints` - hints for the controls panel, read in Node alone, both
611
+ optional:
612
+ - `colorTokens` - the kit's color tokens, such as
613
+ `[null, 'primary', 'accent']`, `null` (or, in `scaffold-ui-kit.ts`,
614
+ `undefined`) being the unset choice. When the list holds any token, a
615
+ string-typed prop whose name ends in `color`, in any case (`color`,
616
+ `borderColor`), gets a select of
617
+ these tokens in place of its own control, and `check` holds the values a
618
+ story file sets for it to the list. Such a prop gets no generated
619
+ scenarios. Without tokens, the prop is treated by its declared type like
620
+ any other: a union is a select with a scenario per value.
621
+ - `componentSizeRem(componentName)` - a function, so in
622
+ `scaffold-ui-kit.ts` alone: the starting value, in rem, of a number prop
623
+ named `size` that the component gives no default, or `undefined` for
624
+ none.
625
+ - `labels` - the preview's own labels, in the kit's casing, read in Node
626
+ alone: `category`, `emptyScenario`, and, in `scaffold-ui-kit.ts`,
627
+ `generatedScenario`, a function from a prop and the value a generated
628
+ scenario sets to its label (by default `variant: solid`, or `disabled` for
629
+ a boolean).
630
+
631
+ A `mount` frame that gives every story a gutter, with an option that paints
632
+ the frame's surface, as keys of the `preview` block:
633
+
634
+ <!-- example: preview-mount -->
635
+ ```ts
636
+ mount: {
637
+ classes: ['story-frame'],
638
+ ref: 'frame',
639
+ children: [{ classes: ['story-gutter'], mount: true }],
640
+ },
641
+ canvas: {
642
+ namePrefix: 'my-ui-kit',
643
+ extraCss: [
644
+ '.story-frame { min-height: 100vh; background: var(--surface); }',
645
+ '.story-gutter { padding: 1.5rem; }',
646
+ ].join('\n'),
647
+ },
648
+ options: [
649
+ {
650
+ id: 'surface',
651
+ label: 'surface',
652
+ values: [
653
+ { value: 'white', label: 'white' },
654
+ { value: 'whitesmoke', label: 'smoke' },
655
+ ],
656
+ default: 'white',
657
+ scope: 'global',
658
+ apply: { type: 'css-var', var: '--surface', target: { ref: 'frame' } },
659
+ },
660
+ ],
661
+ ```
662
+
663
+ The story mounts inside the node marked `mount: true`, so the gutter's
664
+ padding surrounds it; without a marked node it mounts in the outermost one.
665
+ `extraCss` is appended to the stylesheet the preview injects into every
666
+ story's iframe, where it styles the frame's nodes by their classes, and the
667
+ `surface` option sets `--surface` on the node whose `ref` is `frame`, which
668
+ `extraCss` paints with.
669
+
670
+ ### A kit styled with Tailwind
671
+
672
+ A kit built with the `tailwind` styling extension, in its default `classes`
673
+ output, ships utility classes that need a Tailwind build. The preview loads
674
+ that build as the kit's theme. A kit scaffolded with `--styling tailwind`
675
+ gets this setup; another kit adds it in four steps.
676
+
677
+ 1. Install `tailwindcss` and `@tailwindcss/cli` as dev dependencies.
678
+ 2. Write an entry stylesheet, such as `preview/tailwind.css`, that names the
679
+ built components and the story files as sources. Tailwind skips git-ignored
680
+ files, `dist` among them, unless a `@source` names them:
681
+
682
+ ```css
683
+ @import "tailwindcss";
684
+
685
+ @source "../dist";
686
+ @source "../src/components";
687
+ @source "./stories";
688
+
689
+ /* dark: utilities follow a mode option that sets data-mode="dark" */
690
+ @custom-variant dark (&:where([data-mode="dark"], [data-mode="dark"] *));
691
+ ```
692
+
693
+ 3. Build the kit and compile it before the preview starts or builds:
694
+
695
+ ```json
696
+ {
697
+ "scripts": {
698
+ "preview:css": "tailwindcss --input preview/tailwind.css --output preview/tailwind.generated.css",
699
+ "preview": "npm run build && npm run preview:css && scaffold-ui-kit-preview dev",
700
+ "preview:build": "npm run build && npm run preview:css && scaffold-ui-kit-preview build"
701
+ }
702
+ }
703
+ ```
704
+
705
+ The other targets' panes show the built components, and Tailwind reads their
706
+ classes from `dist`, so the kit builds first. `generate` and `build` copy
707
+ the theme as it is, so `preview:css` has to run before either, including
708
+ when they run on their own.
709
+
710
+ 4. Load the compiled file as a theme, in the `preview` block:
711
+
712
+ <!-- example: preview-tailwind-theme -->
713
+ ```ts
714
+ themes: [
715
+ { id: 'tailwind', label: 'Tailwind', css: 'preview/tailwind.generated.css' },
716
+ ],
717
+ ```
718
+
719
+ The `@custom-variant` line pairs with a `root-attribute` option, such as the
720
+ `mode` option above, so a story's `dark:` utilities follow it. The dev server
721
+ reloads the theme when the compiled file changes, so
722
+ `npm run preview:css -- --watch` beside it follows class changes once the kit
723
+ is built again. Ignore the compiled file in `.gitignore`.
724
+
725
+ A kit with `tailwind: { output: 'styles' }` has no Tailwind build: it sets
726
+ `tailwind.darkSelector: '[data-mode="dark"]'` instead, so its converted
727
+ `dark:` styles follow the same option.
728
+
729
+ ### A kit whose stylesheets are not linked
730
+
731
+ A kit built with `stylesheetLink: 'none'` emits each component's stylesheet
732
+ and links it from nowhere, so the other targets' panes show its components
733
+ unstyled. The html pane reads the html target's stylesheets itself. Give the
734
+ other targets' panes the same styles as a theme: join the kit's reset and one
735
+ scripted target's built stylesheets into one file, and load that file as
736
+ the theme.
737
+
738
+ ```json
739
+ {
740
+ "scripts": {
741
+ "preview:css": "cat src/styles/reset.css dist/react/*.css > preview/components.generated.css",
742
+ "preview": "npm run build && npm run preview:css && scaffold-ui-kit-preview dev",
743
+ "preview:build": "npm run build && npm run preview:css && scaffold-ui-kit-preview build"
744
+ }
745
+ }
746
+ ```
747
+
748
+ <!-- example: preview-unlinked-theme -->
749
+ ```ts
750
+ themes: [
751
+ { id: 'default', label: 'Default', css: 'preview/components.generated.css' },
752
+ ],
753
+ ```
754
+
755
+ The preview copies a theme as it is, so an `@import` in it would not resolve:
756
+ join the files instead. A kit built with `stylingLanguage: 'scss'` compiles
757
+ its sheets with sass into that file instead. A kit using Tailwind's `output: 'styles'` needs a reset
758
+ that works without a Tailwind build (see the Tailwind extension's README):
759
+ Tailwind's own `preflight.css` reads its theme through `--theme()`, which
760
+ only a Tailwind build resolves.
761
+
762
+ ## Custom views
763
+
764
+ A `viewsModule` exports views: panels the preview lists beside the component
765
+ stories and mounts in an iframe of their own, under the live theme and
766
+ options, such as a tokens page or an icon gallery. A view is
767
+ `{ id, label, source?, order?, framework?, mount }`. The iframe loads the
768
+ kit's setup module for the view's `framework` (`react`, `vue`, `svelte`, or
769
+ `dom`, the default, which loads none), then calls
770
+ `mount(element, context, updates)`, which renders into `element` and may
771
+ return a function that unmounts it. `context` holds the current option
772
+ values, the kit's sources and component index (the stories a reader
773
+ browses, unlisted ones left out), and `selectComponent(id, props?)`, which
774
+ opens a component's story, an unlisted one included; `updates.subscribe(listener)`
775
+ delivers a new context each time the options change or the catalog arrives,
776
+ and the view stays mounted throughout. `reactView(render)` makes a React view
777
+ from a render function, re-rendering it in place with each context. An entry
778
+ `{ id, label, componentTree: true }` places the source's component tree among
779
+ its views. To open the preview on a view, such as a foundations page, name
780
+ it as the block's `landing` entry: `"landing": { "view": "foundations" }`.
781
+
782
+ <!-- example: preview-views -->
783
+ ```tsx
784
+ import { reactView, type PreviewView } from '@scaffold-ui-kit/preview/views';
785
+
786
+ export const views: PreviewView[] = [
787
+ {
788
+ id: 'foundations',
789
+ label: 'Foundations',
790
+ order: 10,
791
+ ...reactView((context) => <Foundations theme={String(context.options.theme)} />),
792
+ },
793
+ {
794
+ id: 'swatches',
795
+ label: 'Swatches',
796
+ order: 20,
797
+ mount(element, context, updates) {
798
+ const paint = (current: typeof context) => {
799
+ element.textContent = `theme: ${String(current.options.theme)}`;
800
+ };
801
+ paint(context);
802
+ return updates.subscribe(paint);
803
+ },
804
+ },
805
+ { id: 'components', label: 'Components', order: 30, componentTree: true },
806
+ ];
807
+
808
+ export const viewById = (id: string) => views.find((view) => view.id === id);
809
+ ```
810
+
811
+ ## A code block of the kit's own
812
+
813
+ The code view shows each target's source of the story in the shell's own
814
+ code block. A kit with a code block of its own, such as a highlighted one in
815
+ the kit's look, names it as `codeBlock`: a module whose default export mounts
816
+ the block, and the framework it mounts with (`dom`, the default, loads no
817
+ framework's setup module). The preview mounts it in an iframe in each code
818
+ pane, as it mounts a custom view, and sends it the target's source, its
819
+ language (`tsx`, `vue`, `svelte` or `html`), or why the target has none,
820
+ each time the story, its props or its content change. Each content also
821
+ carries `wrap`, whether the shell wraps its code panes' long lines (the
822
+ stock shell's `w`), sent again each time it is toggled: the block wraps long
823
+ lines while it is true and scrolls sideways while it is false. A shell with
824
+ no such choice sends `false`. The block shows source, not a story: it mounts
825
+ into an element that fills its iframe, with no `mount` wrapper around
826
+ it, and only the global options apply there, so it follows the page's mode
827
+ and theme while the story options (a box, padding) leave it alone. The
828
+ iframe's page is the block's: its root carries `data-preview-frame="code"`,
829
+ and the kit may style the frame's `html` and `body` by it, for example in
830
+ the block's background so an overscroll bounces over the same color.
831
+
832
+ ```css
833
+ html[data-preview-frame='code'] { background: var(--code-background); }
834
+ ```
835
+
836
+ <!-- example: preview-code-block-configuration -->
837
+ ```ts
838
+ codeBlock: { module: 'preview/code-block.ts' },
839
+ ```
840
+
841
+ <!-- example: preview-code-block -->
842
+ ```ts
843
+ import type { CodeBlockMount } from '@scaffold-ui-kit/preview/views';
844
+
845
+ const mount: CodeBlockMount = (element, content, updates) => {
846
+ const show = (next: typeof content) => {
847
+ const pre = document.createElement('pre');
848
+ pre.style.whiteSpace = next.wrap ? 'pre-wrap' : 'pre';
849
+ pre.textContent = next.source ?? next.reason ?? '';
850
+ element.replaceChildren(pre);
851
+ };
852
+ show(content);
853
+ return updates.subscribe(show);
854
+ };
855
+
856
+ export default mount;
857
+ ```
858
+
859
+ ## Links and persistence
860
+
861
+ The preview keeps what is showing in its address, so a link opens it as it
862
+ was. These query parameters are the preview's:
863
+
864
+ - `component=<id>` or `view=<id>` - the selected story or view, a story by
865
+ the id described under the testing helpers below, `components-taginput` for
866
+ a `TagInput` in the default category,
867
+ - `scenario=<id>` - the story's scenario, when it is not the one the story
868
+ starts on,
869
+ - `props=<JSON>` - the props that differ from the scenario's, as one JSON
870
+ object, so a story whose props hold a lot of data keeps a short link. A
871
+ prop the link leaves out takes the scenario's value, the story file's
872
+ `defaultProps` beneath it, so a hand-written link can name just the props
873
+ it changes. A prop of the scenario's removed from the story is written as
874
+ `null`, which a link reads as left out. Picking a scenario or another
875
+ story clears them. The scenario editor's content never goes in the address, apart from
876
+ the root's lone text where it is the story's `children`,
877
+ - `targets=<list>` - the visible targets, comma-separated, `html`
878
+ among them where the kit builds it (`targets=react,svelte,html`), while
879
+ not every one of the kit's is on,
880
+ - `width=<pixels>` - the width the story panes are laid out at, while they
881
+ do not fit the space the shell gives them (`width=768`). A link can name
882
+ any whole number of pixels up to 7680, whether or not the kit's
883
+ `paneWidths` list it,
884
+ - `option.<id>=<value>` - each option set away from its default (`true` or
885
+ `false` for a boolean), a shell's own as `option.shell.<id>`. A story
886
+ option appears only while its story is selected.
887
+
888
+ An address that names no story or view opens on the kit's `landing` entry,
889
+ or on the first story the navigation lists without one.
890
+
891
+ Opening a link reads them all, and what the kit does not know is ignored: an
892
+ unknown story, scenario or target, a value an option cannot hold, and in
893
+ `props` a prop the component does not declare or a value its control does
894
+ not accept (such a value keeps the scenario's).
895
+
896
+ The preview writes the address from the first change a person makes on, in
897
+ place, adding no browser history. A page that is only opened keeps the
898
+ address it was opened with, so a shell's own pages stay as they are linked.
899
+ The preview rewrites only its own parameters and keeps every other query
900
+ parameter, the path and the hash, so a shell may keep state of its own in
901
+ the address, such as a tab or a route. It is all query parameters, so links
902
+ work on the static build too.
903
+
904
+ `persistence` decides what else is remembered. `"url"`, the default, keeps
905
+ everything in the address alone. `"url-and-local-storage"` also remembers the
906
+ global option values for this kit in the browser across sessions, a value in
907
+ the address winning over a remembered one. The selection, the scenario, the
908
+ props, the visible targets and the pane width always live in the address
909
+ alone.
910
+
911
+ ## The charcell shell
912
+
913
+ `@scaffold-ui-kit/preview/shells/charcell` draws the preview in
914
+ [charcell](https://www.npmjs.com/package/charcell)'s terminal look:
915
+
916
+ - a top bar with the kit and its version, the kit's own settings (the `kit`
917
+ group), and two groups of toggles: the views, `navigation`, then `scenario`,
918
+ `output`, `code` and `events`, the stage's four rows, and the targets, its
919
+ columns;
920
+ - the navigation, the kit's components and views, one tab stop moved through
921
+ with the arrow keys;
922
+ - a stage bar with a select of everything the navigation lists, with an info button for the component's description,
923
+ its markdown rendered;
924
+ - the stage's views, each under a heading row whose end holds the view's
925
+ options and controls: the scenario row, with the scenario picker and the
926
+ props and content editor; the output row, a preview pane per target, with
927
+ the context the story is shown in (the story's options and the global
928
+ options of the `context` group, or of none) and a `width` select in an
929
+ options popover; and
930
+ the code row, each target's source under its pane, with a `wrap` toggle;
931
+ and the events row, a log of the events the story's panes fire: each call
932
+ of an event the component declares, a function prop that returns nothing,
933
+ such as `onSelect`, with the pane and what it was called with, and a
934
+ `clear`. A function prop that returns something, such as a formatter, is
935
+ left as the story gives it;
936
+ - and a status bar with the panes' state, a link to what the preview shows,
937
+ the shell's keys and any other group of the kit's.
938
+
939
+ A kit that names it installs its two peers beside the preview:
940
+
941
+ ```sh
942
+ npm install --save-dev charcell @zag-js/react
943
+ ```
944
+
945
+ `check`, `build` and `dev` name either peer the kit has not installed,
946
+ before anything is bundled, whether the kit names the shell as it is or
947
+ through a module of its own that configures it (below).
948
+
949
+ Named as it is, it comes with no content of the kit's. To fill its slots or
950
+ move an option group, the kit names a module of its own as its `shell`, which
951
+ configures it, and the charcell shell's page beside it:
952
+
953
+ <!-- example: preview-shell-configuration -->
954
+ ```ts
955
+ shell: {
956
+ module: 'preview/shell.tsx',
957
+ page: '@scaffold-ui-kit/preview/shells/charcell/page',
958
+ },
959
+ ```
960
+
961
+ The page is what the host page shows before the shell has loaded: charcell's
962
+ dark ground from the first frame, its face preloaded, and a `loading` note
963
+ set in that face on a whole cell, after a moment. Named as it is, the shell
964
+ brings the page itself; a module of the kit's names it, as above, or starts
965
+ from an empty page, which `check`, `build` and `dev` warn of.
966
+
967
+ <!-- example: preview-charcell-shell -->
968
+ ```tsx
969
+ import { charcellShell, reactFill } from '@scaffold-ui-kit/preview/shells/charcell';
970
+ import { usePreview } from '@scaffold-ui-kit/preview/react';
971
+
972
+ export default charcellShell({
973
+ slots: {
974
+ 'top-bar-end': reactFill((controller) => {
975
+ const selection = usePreview(controller, (state) => state.selection);
976
+ return <span>{selection.id}</span>;
977
+ }),
978
+ 'status-bar': (element, controller) => {
979
+ element.textContent = `${controller.catalog.kitName} ${controller.catalog.kitVersion ?? ''}`;
980
+ },
981
+ },
982
+ groups: { canvas: 'status-bar' },
983
+ });
984
+ ```
985
+
986
+ - `slots` - content for the shell's named slots: `top-bar-end`, at the end
987
+ of the top bar after the target toggles; `stage-bar-end`, at the end of the
988
+ stage bar; `scenario-end`, `output-end`, `code-end` and `events-end`, at
989
+ the end of each view's heading row, before the view's own control; and
990
+ `status-bar`, at the end of the status bar.
991
+ A fill is `(element, controller) => unmount`. It mounts into `element` in
992
+ any framework or in plain DOM, reads and drives the preview through
993
+ `controller` (below), and may return a function that unmounts it.
994
+ `reactFill(render)` makes a fill from a React render function, rendered as
995
+ a component of its own, so it can use hooks such as `usePreview`.
996
+ - `groups` - where each option group goes, by group name: `top-bar`,
997
+ `stage-bar`, `scenario`, `output`, `code`, `events` or `status-bar`, for
998
+ options of any scope. Without an entry, a story option and a global option
999
+ of the `context` group (or of none) sit with the output, a global option of
1000
+ the `kit` group in the top bar, and any other group in the status bar.
1001
+ - `popovers` - which views hold their options in an options popover at the
1002
+ end of their heading row rather than in the row itself, such as
1003
+ `{ output: false, code: true }`. The output's are in a popover unless it
1004
+ says otherwise.
1005
+
1006
+ Each view can be hidden and shown, and each target, so a reader keeps the
1007
+ rows and columns they want. The panes stay loaded while views come and go,
1008
+ and the other panes while a target does; a row or column comes back at the
1009
+ size it had. The views are the shell's own
1010
+ options, so the address keeps them (`option.shell.scenario=true`,
1011
+ `option.shell.output=false`, `option.shell.code=true`) and a link opens the
1012
+ story as it was laid out. The output row shows to start with; with no view
1013
+ showing, the stage says how to show one.
1014
+
1015
+ The scenario row starts at its content's height, at most half the stage. Its editor
1016
+ is open while the row shows, where the preview can regenerate: hiding the row
1017
+ closes it and keeps its tree until another story or scenario is picked. In
1018
+ the static build the row holds the picker, with a note that editing a
1019
+ scenario needs the preview's development server.
1020
+
1021
+ The output and code rows sit in a grid that resizes both ways, its columns
1022
+ the visible targets. The columns start at equal widths. A target turned off
1023
+ gives its width to the others in proportion to theirs, so three equal
1024
+ columns become two halves, and 40/20/40 without the last becomes 67/33; a
1025
+ target turned back on returns at the width it had, the others giving way in
1026
+ proportion. The output row starts at the height of its stories,
1027
+ until it is resized. A double-click on a handle, or Enter while it has the
1028
+ focus, puts its split back where it started: the columns at equal widths,
1029
+ the output and code rows at their starting heights. The code row shows, under each pane, the story's code
1030
+ for that target, or its html under the html pane, with a copy button; the
1031
+ `wrap` toggle wraps every code pane's long lines, on to start with and kept
1032
+ in the address while off (`option.shell.wrap=false`).
1033
+
1034
+ The `width` select, the last of the output's options, fits each pane to
1035
+ its column (`fit`, to start with), or lays every story pane out at one of
1036
+ the kit's pane widths, such as `medium 768`, each pane as tall as its
1037
+ story. While that width is wider
1038
+ than a column would be, the output stacks: a row per target, each the
1039
+ stage's full width, with the code panes side by side in the code row under
1040
+ them. Each row starts at its story's height, and the space left under the
1041
+ stories goes to the code row, or to the last row without one. When the
1042
+ stories are too tall to fit together, the rows share the stage equally,
1043
+ and a story shorter than its share keeps its own height. A row keeps at
1044
+ least ten rows of text, its frame and about seven rows of story, or its
1045
+ whole story when that takes fewer; the code row keeps eight. When those
1046
+ don't fit on the stage together, as with several tall stories and the code
1047
+ row on a short window, the stage scrolls down instead of squeezing a row.
1048
+ The rows follow the stories and the stage until you drag a handle between
1049
+ them or step it with a key, and again after a double-click or Enter on one
1050
+ of those handles. A story
1051
+ taller than its row scrolls down, and one wider than its row scrolls
1052
+ across, with a scrollbar on a row of its own, so it never covers the
1053
+ story; one narrower than its column is centered in it.
1054
+
1055
+ The shell's CSS custom properties are its styling surface; its class names
1056
+ and markup are its own.
1057
+
1058
+ ## Writing a shell
1059
+
1060
+ A shell is the whole interface around the panes: the navigation, the option
1061
+ controls, the scenario picker, the layout and the scenario editor. The
1062
+ preview provides the behavior as a controller, and the shell renders it. It
1063
+ decides which options show and as which controls, and where the panes go. The
1064
+ preview owns the iframes: a shell hands it an element to mount a pane in and
1065
+ never talks to a pane itself.
1066
+
1067
+ A shell module default-exports a shell made with `defineShell` from
1068
+ `@scaffold-ui-kit/preview/shell`. That entry also exports everything a shell
1069
+ reads: the controller's types, the catalog's, the editor tree's and the
1070
+ option helpers.
1071
+
1072
+ <!-- example: preview-define-shell -->
1073
+ ```ts
1074
+ import { defineShell } from '@scaffold-ui-kit/preview/shell';
1075
+
1076
+ export default defineShell({
1077
+ apiVersion: 1,
1078
+ options: [{ id: 'dense', label: 'dense', default: false }],
1079
+ mount(root, controller) {
1080
+ // render into root, read and drive controller
1081
+ return () => root.replaceChildren();
1082
+ },
1083
+ });
1084
+ ```
1085
+
1086
+ - `apiVersion` - the contract version the shell was written against, `1`.
1087
+ The preview refuses a shell of a version it does not serve, naming both.
1088
+ - `options` - the shell's own options, such as its density, in the shape of a
1089
+ kit option without `apply` or `scope`. The preview keeps their values with
1090
+ the kit's, under `shell.<id>` in the state and the address
1091
+ (`shellOptionId('dense')` gives `'shell.dense'`), and never sends them to
1092
+ the panes. They are global. `options` may also be a function of the
1093
+ catalog, called once before the preview starts, for an option that depends
1094
+ on the kit, such as a choice among its targets:
1095
+ `options: (catalog) => [{ id: 'pane', label: 'pane', values:
1096
+ catalog.targets.map((target) => ({ value: target, label: target })),
1097
+ default: catalog.targets[0] }]`.
1098
+ - `mount(root, controller)` - renders the shell into the host page's root
1099
+ element, which the preview empties first, and may return a function that
1100
+ unmounts it.
1101
+
1102
+ The host page provides little: a `<title>`, a viewport `<meta>` for small
1103
+ screens, the root element and a script that mounts the shell. It has no CSS
1104
+ of its own, so the body keeps the browser's default margin; from the root
1105
+ element on, the document is the shell's to style and lay out.
1106
+
1107
+ ### The page before the shell mounts
1108
+
1109
+ The shell's script takes a moment to load, on the development server a few
1110
+ seconds. What the host page holds until then is the shell's too, given by a
1111
+ page module: a module evaluated in Node as the preview starts or builds,
1112
+ whose default export is a page made with `defineShellPage` from
1113
+ `@scaffold-ui-kit/preview/shell`.
1114
+
1115
+ <!-- example: preview-shell-page -->
1116
+ ```ts
1117
+ import { fileURLToPath } from 'node:url';
1118
+ import { defineShellPage } from '@scaffold-ui-kit/preview/shell';
1119
+
1120
+ const font = fileURLToPath(new URL('./fonts/face.woff2', import.meta.url));
1121
+
1122
+ export default defineShellPage({
1123
+ head: ({ asset }) =>
1124
+ `<link rel="preload" as="font" type="font/woff2" crossorigin href="${asset(font)}" />
1125
+ <style>html { background: #101010; }</style>`,
1126
+ root: '<p role="status">loading</p>',
1127
+ });
1128
+ ```
1129
+
1130
+ - `head` - markup written at the end of the page's `<head>`: styles, a font
1131
+ face, links that preload what the shell will need. It stays for the life
1132
+ of the page.
1133
+ - `root` - markup written into the root element, shown until the shell
1134
+ mounts: a loading note, with the styles only it needs. Mounting empties
1135
+ the root, so nothing of it is left under the shell.
1136
+
1137
+ Each is markup, or a function that returns it, which may be asynchronous.
1138
+ A function is given `asset(file)`: the address of a file the markup
1139
+ references, by its absolute path. The development server serves the file
1140
+ from where it is, and the static build copies it into its output, so write
1141
+ the address `asset` gives into an `href`, a `src` or a CSS `url()` rather
1142
+ than a path of your own. The module runs in Node, so it reads files and
1143
+ resolves packages freely, and holds nothing that needs a browser. It is
1144
+ loaded once, when the preview starts: restart `dev` after editing it.
1145
+
1146
+ The preview finds the page two ways. A shell named as a package specifier
1147
+ brings its own: for `shell: '@acme/shells/compact'` the preview uses the
1148
+ module at `@acme/shells/compact/page` when the package exports one, under
1149
+ the `default` condition as the shell itself is. And a
1150
+ kit names one in the long form of `shell`, as a package specifier or a
1151
+ module path relative to the kit root, which is how a shell in the kit gets
1152
+ a page:
1153
+
1154
+ <!-- example: preview-shell-page-configuration -->
1155
+ ```ts
1156
+ shell: { module: 'preview/shell.ts', page: 'preview/page.ts' },
1157
+ ```
1158
+
1159
+ A shell with no page starts from an empty root. `check` loads the page as
1160
+ `dev` and `build` do, and reports one that cannot be loaded or is no page.
1161
+
1162
+ A shell takes its own settings through a factory of its own, as
1163
+ `charcellShell({ slots, groups })` does. The `preview` block reaches the
1164
+ panes as JSON data, so it is no place for them.
1165
+
1166
+ ### The controller
1167
+
1168
+ `controller.catalog` is what stays put for the session:
1169
+
1170
+ - `kitName`, `kitVersion`, and `targets`, the kit's `targets`, in order:
1171
+ one pane and one toggle each. `html` comes last where the kit builds it.
1172
+ The shell entry types them as `PreviewTarget` (`PREVIEW_TARGETS`), and the
1173
+ targets other than `html` as `PreviewScriptedTarget`
1174
+ (`PREVIEW_SCRIPTED_TARGETS`).
1175
+ - `navigation` - the tree of stories and views to list, as `view`,
1176
+ `component` and `group` entries, the components that are not standalone
1177
+ and unlisted stories left out.
1178
+ - `components` - every component, `standalone` (`false` for a compound's
1179
+ parts and the others with no story of their own) and
1180
+ stories kept out of what a reader browses marked `unlisted`, with
1181
+ its `title`, `shortTitle`, scenarios, props, slots and `targets`, and its
1182
+ `description` from the template when it has one, as each scenario has its
1183
+ story file's. A scenario's `options` are the option
1184
+ values picking it sets, by option id. Each
1185
+ entry of `propTypes` is one prop: its control (`control`, `options`,
1186
+ `number`, and `include`, `false` for a prop shown no control), its
1187
+ `description`, and what the component declares: `declaredType`, the
1188
+ type as written (`"'solid' | 'ghost'"`), `required`, and
1189
+ `declaredDefault`, the component's own default, absent when it has none,
1190
+ and `documentedDefault`, a default the component applies without declaring
1191
+ it, as the code a consumer reads (`"'start'"`). The component's, each
1192
+ scenario's and each prop's `description`, and an option's `title`, are
1193
+ markdown, handed out as written for a shell to render; the stock shell
1194
+ renders them, showing raw html in them as text.
1195
+ `defaultProps` is where the story starts, which can differ: it adds the
1196
+ story file's `defaultProps` and the kit's hints over the declared defaults.
1197
+ `events` names the props that are events, functions that return nothing
1198
+ such as `onSelect`, which the panes record. `bindings` lists the
1199
+ component's two-way props, those whose template declares a `bindable`
1200
+ object, in prop order: each as `{ prop, defaultProp, changeProp }`, the
1201
+ latter two absent when the template names none, with `vueModel: true` on
1202
+ the one a Vue consumer binds with a bare `v-model`. When the story gives
1203
+ `prop` and a pane reports `changeProp`, the controller sets the reported
1204
+ value as the story's `prop`, as `setProps` would: every pane, the html and
1205
+ the address follow, and the event is logged as any other. A story that
1206
+ leaves `prop` out lets each pane's component keep its own value.
1207
+ - `views`, `sources`, `htmlTags` (the tags an editor offers for a wrapper) and
1208
+ `colorTokens` (the values a color control offers).
1209
+ - `paneWidths` - the widths to offer for laying the story panes out at, as
1210
+ `{ label, width }` in CSS pixels, narrowest first: the `preview` block's
1211
+ `paneWidths`, else `DEFAULT_PANE_WIDTHS` (`small` 375,
1212
+ `medium` 768, `large` 1280) from the shell entry.
1213
+ - `landing` - the kit's `landing` entry, as `{ kind: 'view', id }` or
1214
+ `{ kind: 'component', id, scenario? }`, absent without one. The controller
1215
+ starts on it when the address names no story or view.
1216
+
1217
+ `controller.options` lists the options to show controls for, the kit's and
1218
+ then the shell's, each with an `origin` of `kit` or `shell`.
1219
+ `optionKind(option)` gives `choice` or `boolean`, and `optionGroup(option)`
1220
+ the group, `context` (`CONTEXT_OPTION_GROUP`) when none is set; the kit's own
1221
+ settings are `kit` (`KIT_OPTION_GROUP`).
1222
+
1223
+ `controller.getState()` is the current state, and
1224
+ `controller.subscribe(listener)` calls the listener with each new one and
1225
+ returns a function that stops it. A state is plain data, replaced on each
1226
+ change with its unchanged fields kept by reference:
1227
+
1228
+ - `selection` - `{ kind: 'component' | 'view', id }`.
1229
+ - `scenario` - the selected scenario's id, `''` for the default.
1230
+ - `props` - the story's root props the panes show: the scenario's over the
1231
+ component's defaults, then whatever `setProps` or the editor's root set.
1232
+ The editor holds a string `children` the story draws its default slot from
1233
+ as the root's text, a text node in that slot, so it can be edited, wrapped
1234
+ or replaced like any node; while the editor holds a tree, `children` is
1235
+ that text when the default slot holds one text node and nothing else, and
1236
+ absent otherwise, as other content there is what the panes show in its
1237
+ place. A story with default-slot content of its own (from its story file)
1238
+ shows that content and not `children`, so its props carry no `children`
1239
+ while the editor holds a tree.
1240
+ - `options` - every option's value, by id.
1241
+ - `unavailableOptions` - the options whose controls to hide: those the
1242
+ selected story marks unavailable, and every story option while a view is
1243
+ selected.
1244
+ - `targets` - `supported` (the selected story's), `visible` (turned on)
1245
+ and `panes`, the ones to mount, in order. Where the kit builds html, every
1246
+ story supports `html`; a story with no markup has its html pane say why.
1247
+ - `paneWidth` - the width in CSS pixels to lay each of the selection's story
1248
+ panes out at, html's among them, so a component that responds to its
1249
+ width shows as it does on a screen that wide: usually one of
1250
+ `catalog.paneWidths`, though a link can name any. Absent while the panes
1251
+ fit the shell's space. The shell sizes the iframe to it and decides how a
1252
+ pane narrower than that shows, scrolling across it or scaling it down. It
1253
+ outlasts the story; a view's pane does not follow it.
1254
+ - `frames` - the readiness of the selection's panes, `loading` or `ready`,
1255
+ by target (`html` among them), and of a view's pane under `view`.
1256
+ - `storyFrames` - the readiness of each detached story pane (below), by the
1257
+ key `storyFrameKey(target, story)` gives.
1258
+ - `frameSizes` and `storyFrameSizes` - the size of what each pane shows,
1259
+ `{ width, height }` in whole CSS pixels, beside `frames` and `storyFrames`
1260
+ and under the same keys: the story or view with the room the kit's canvas
1261
+ puts around it, as the pane last reported it. An entry appears once the
1262
+ pane renders, changes whenever its content does, keeps its last size while
1263
+ the pane reloads, and goes when the pane is unmounted. The width is the
1264
+ content's at the pane's current width, since block content fills the pane.
1265
+ - `frameErrors` - what each of the selection's panes threw as its story
1266
+ rendered, the error's message by target, which the pane also shows in the
1267
+ story's place. An entry goes when the pane mounts a story that renders,
1268
+ and when the pane is unmounted. The stock shell's status bar names the
1269
+ targets (`render error · react`), their messages in a hint over them.
1270
+ - `events` - the events the selection's panes recorded, oldest first and the
1271
+ last hundred at most: each call a story made of one of its events (a
1272
+ component's `events` in the catalog), as `{ id, target, name, payload,
1273
+ time }`. `id` counts the events recorded, for a stable key; `target` is
1274
+ the pane it was recorded in; `name` is the function prop called;
1275
+ `payload` is a one-line summary of what it was called with; `time` is in
1276
+ milliseconds since the epoch. The log empties when another story or a
1277
+ view is selected, and on `actions.clearEvents()`.
1278
+ - `editor` - whether the editor is `open`, its `tree`, and `modified`: whether
1279
+ the tree's content differs from what it was seeded with, a node added,
1280
+ removed, moved or replaced, or a nested node's props or a text changed.
1281
+ The root's props do not count, as they are `props`; a change of the root's
1282
+ text counts, though `props` shows it as `children`. Content is compared by
1283
+ value, so an edit undone by hand is no change, and a new seed (another
1284
+ story, or another scenario with the editor open) starts unmodified. While
1285
+ it is `true`, the address may not reproduce the panes.
1286
+ - `regeneration` - the editor's regeneration, `idle`, `pending` or `error` with a
1287
+ `message`.
1288
+ - `code` - the code of what the selection's panes show (below), absent
1289
+ while a view is selected.
1290
+
1291
+ `controller.actions` change it:
1292
+
1293
+ - `select(selection)` shows what a navigation entry names, a story or a
1294
+ view; `selectComponent(id, props?)` and `selectView(id)` do the same for
1295
+ one kind.
1296
+ - `clearEvents()` empties `events`.
1297
+ - `setPaneWidth(width)` lays the story panes out at `width` pixels, and
1298
+ `setPaneWidth(undefined)` fits them again. A width that is not a whole
1299
+ number from 1 to 7680 (`MAXIMUM_PANE_WIDTH`) is ignored.
1300
+ - `pickScenario(id)`, `setOption(id, value)` and
1301
+ `setTargetVisible(target, visible)`. Picking a scenario, or selecting
1302
+ a story on its baseline scenario, sets the option values the scenario
1303
+ carries (`scenario.options`) as `setOption` would, so the panes, the
1304
+ controls and the address follow. Values it does not set stay as they are,
1305
+ and picking another scenario leaves them, apart from those the next one
1306
+ sets. A shell option's value never reaches the panes, so setting one posts
1307
+ nothing to them.
1308
+ - `setProps(props)` replaces the story's root props, live, with no
1309
+ regeneration, whether or not the editor is open; the editor's root takes
1310
+ them too. It does not merge: pass `{ ...state.props, tone: 'accent' }` to
1311
+ change one. A prop set to `undefined` is dropped, and it does nothing while
1312
+ a view is selected. `selectComponent(id, props)` lands its props the same
1313
+ way, over the story's defaults. While the editor holds a tree, `children`
1314
+ is the root's text, as `props` shows it: a new string replaces the root's
1315
+ default-slot content with that text, and leaving `children` out removes a
1316
+ lone text. Either is an edit of the tree's content, which regenerates the
1317
+ story, so a shell passes `{ ...state.props, ... }` to keep the text. A
1318
+ story with default-slot content of its own takes no `children` then.
1319
+ - `scenarioChoices(component,
1320
+ controller.capabilities)`, from the shell entry, lists a scenario picker's
1321
+ choices, the default first, and none when there is no choice to make. It
1322
+ leaves out the Empty scenario where the preview cannot regenerate, as in
1323
+ the static build, and `pickScenario` ignores Empty there.
1324
+
1325
+ `controller.frames` places the panes:
1326
+
1327
+ - `mount(element, target)` puts a target's pane in `element` and
1328
+ `mountView(element, viewId)` a view's. Each returns a function that removes
1329
+ it. The controller loads the iframe, reloads it after each regeneration and
1330
+ answers its messages.
1331
+ - `mount(element, 'html')` puts the html pane in `element`. It shows the
1332
+ markup of `code.html` (below) as the story, its props or the editor's
1333
+ tree change, keeps what it shows while the next is printed, and shows the
1334
+ reason when there is none. It loads the kit's theme, canvas and
1335
+ `mount` frame and follows the options as the other panes do,
1336
+ but loads no framework and no setup module, and runs none of the story's
1337
+ behavior. A regeneration does not reload it.
1338
+ - A pane is a bare `<iframe>` appended to `element`, with no size or styling
1339
+ of its own beyond the browser's defaults. The shell sizes it, for example
1340
+ `element > iframe { width: 100%; height: 100%; border: 0 }` inside an
1341
+ element that has a size.
1342
+ - To size a pane to its story instead, as a documentation page or a grid of
1343
+ variants does, set the iframe's height to the `height` in `frameSizes` (or
1344
+ `storyFrameSizes`), with a fallback until the first report. The size does
1345
+ not depend on the iframe's own height, so a pane sized to it stays put: a
1346
+ canvas that fills the viewport (`min-height: 100vh`) adds only its padding.
1347
+ The pane measures itself and posts the size to the host, so a shell never
1348
+ reaches into the iframe.
1349
+ - The pane's readiness in `frames` changes after `mount` returns, never
1350
+ during the call, so a subscriber does not run inside the shell's own
1351
+ mounting code.
1352
+ - `reload(target)` loads the selection's pane for `target` (or a view's
1353
+ pane, as `'view'`) again: its readiness goes back to `loading` until it
1354
+ answers, and it shows the selection as it is now.
1355
+ - `standaloneUrl(target)` gives the address of the selection's pane for a
1356
+ scripted target opened on its own, outside the preview, for a shell's
1357
+ "open in a window" action: the story it shows now (the editor's content
1358
+ while it holds a tree), with its props and the kit's option values. The
1359
+ page follows nothing after it opens.
1360
+ - Every iframe the controller mounts (a pane, a view, a detached story, a
1361
+ code block) stays hidden until it has painted its first content, and again
1362
+ while it reloads, so the shell's own ground shows rather than the
1363
+ browser's blank page and what the pane paints on the way. Once shown, the
1364
+ iframe carries `data-preview-shown` (`PREVIEW_FRAME_SHOWN_ATTRIBUTE`), so a
1365
+ shell's CSS can draw a loading note in the pane until then:
1366
+ `.pane:has(> iframe:not([data-preview-shown]))::after { content: 'loading' }`.
1367
+ The stock shell does, after 300ms.
1368
+ - `mountStory(element, target, { component, scenario?, props? })` puts a
1369
+ read-only pane of any story in `element`, apart from the selection, for a
1370
+ grid of variants or a documentation page that lists every scenario live.
1371
+ Its props are the scenario's over the component's defaults, with `props`
1372
+ over those; `scenario` omitted is the one the story starts on, and `''`
1373
+ the anonymous default. It follows the global options, and its story
1374
+ options are the component's own, as selecting the story would seed them;
1375
+ the option values its scenario sets stay over both.
1376
+ It never shows the editor's content and is not reloaded by a
1377
+ regeneration. Its readiness is reported in `storyFrames` under
1378
+ `storyFrameKey(target, story)`, after the call returns, never in
1379
+ `frames`, which stays the selection's. It throws for a component that is
1380
+ unknown or not standalone, a target the component does not render in
1381
+ (`html` among them: a detached pane renders in a scripted target), and an
1382
+ unknown
1383
+ scenario or the Empty one, which renders through the selection alone; list
1384
+ `component.scenarios.filter((scenario) => !scenario.empty)`.
1385
+ - `mountCode(element, target)` puts the kit's own code block (see "A code
1386
+ block of the kit's own") in `element`, showing `target`'s source of the
1387
+ selection. `wrapCode(wrap)` tells every code block whether the shell wraps
1388
+ its code panes' long lines; a shell with that choice calls it as the
1389
+ choice changes, and the blocks see `false` until it does.
1390
+
1391
+ `controller.editor` owns the scenario editor's tree and its operations; drag
1392
+ and drop, if any, is the shell's:
1393
+
1394
+ - `open()` seeds the tree from the selected story, and `close()` hides it.
1395
+ The content beside the root is not the tree's: a shell reads it from the
1396
+ catalog's component (`around`) and shows it as locked, no operation
1397
+ changes it, and every regeneration renders it as the story gives it.
1398
+ - `createNode(request)` makes a node to insert. The request is
1399
+ `{ kind: 'text', value }`, `{ kind: 'html', tag }`, or
1400
+ `{ kind: 'component', name, importAs? }`, `name` being the component's
1401
+ export name and `importAs` what it comes in with: nothing (`'empty'`, the
1402
+ default), its defaults and content (`'default'`), or a scenario's props
1403
+ (`{ scenario }`). The node it returns is the tree's own: a text node
1404
+ `{ kind: 'text', value }`, an HTML node `{ kind: 'html', tag, props, slots }`,
1405
+ or a component's `{ kind: 'element', component, props, slots }`, its
1406
+ `component` the export name.
1407
+ - `insert({ parent, slot, index? }, node)`, `replace(path, node)`,
1408
+ `remove(path)`, `setNodeProps(path, props)` and `setText(path, text)`. A
1409
+ path is a list of steps from the root, each `{ slot, index }`: the slot a
1410
+ node is in and its index there. The empty path `[]` addresses the root, and
1411
+ a parent of `[]` inserts into the root's own slots.
1412
+ - `setNodeProps(path, props)` replaces the node's props with `props`; it
1413
+ does not merge, so pass the props to keep along with the one that changed.
1414
+ For the root, `[]`, it does what `actions.setProps(props)` does, except
1415
+ that it leaves the root's content, and so `children`, as it is: the tree's
1416
+ root props never hold `children`, so a root prop control passes the root's
1417
+ `props` from the tree.
1418
+ - `move(from, to)` moves a node to a drop target, its index counted before
1419
+ the node is taken out, as a drop reports it. `moveBy(path, offset)` moves
1420
+ it within its slot, `-1` up and `1` down, stopping at either end, for up
1421
+ and down buttons. `resolveDrop(drop)` turns what a sortable list reports
1422
+ into the `from` and `to` of a move.
1423
+ - `nodeSlots(catalog, node)`, from the shell entry, lists the slots a node
1424
+ holds content in, the default first: a component's as the catalog lists
1425
+ them, an HTML tag's default slot, none for text. The tree itself stands for
1426
+ its root. `childPath(parent, slot, index)` gives a child's path,
1427
+ `childPath([], 'default', 0)` the root's first child, and `nodeAt(slots,
1428
+ path)` the node at a path, or `null` where there is none (the root is the
1429
+ tree itself, so `nodeAt` of `[]` is `null` too).
1430
+
1431
+ `controller.capabilities.regeneration` is `false` in the static build, which cannot
1432
+ regenerate a story; a shell hides its editor then.
1433
+
1434
+ ### A story's code
1435
+
1436
+ `state.code` holds the code of what the selection's panes show, for a shell
1437
+ to put beside them. It follows the story, its scenario, `props` and, while the
1438
+ editor holds a tree, the tree:
1439
+
1440
+ - `targets` - the code a consumer writes, by target, for each target other
1441
+ than html the story renders in (`targets.supported`): a React component, a
1442
+ Vue single-file component, a Svelte component. It imports each component
1443
+ from the kit's package entry for the target, writes the props the panes show,
1444
+ leaving out a prop that holds the component's declared default or a value
1445
+ the preview starts from on its own, and the slot content, leaving out the
1446
+ component's name the panes show in an unfilled default slot. A two-way prop
1447
+ the story gives is bound to state of the code's own, starting at the
1448
+ story's value, as a consumer controls it:
1449
+
1450
+ | Target | The code holds the value in | and binds it with |
1451
+ |---|---|---|
1452
+ | React | `const [open, setOpen] = useState(true);` | `open={open} onOpenChange={setOpen}` |
1453
+ | Vue | `const open = ref(true);` | `v-model:open="open"`, or `v-model="open"` for the prop the component names its model |
1454
+ | Svelte | `let open = $state(true);` in runes mode, `let open = true;` in legacy mode, as the kit builds Svelte | `bind:open` |
1455
+
1456
+ The state takes the prop's declared type where the value alone would
1457
+ read narrower, such as a union of strings. Where the declared type names
1458
+ types the kit's package entry exports, such as an interface a template
1459
+ declares with `export` in its `imports`, the state takes it as declared and
1460
+ the code imports them from the entry (`useState<DateInputValue>` beside
1461
+ `import { DateInput, type DateInputValue }`). Where it names other types, or
1462
+ where nothing is declared and the value is an array, an object or `null`,
1463
+ the state takes the component's own prop type, `ComponentProps<typeof
1464
+ Disclosure>['open']`; for a component with type parameters the value infers
1465
+ it. React binds a prop only when the component has a change prop to report
1466
+ it through; without one it prints the value. A story that leaves the prop
1467
+ out prints none of this, and neither does one whose value equals the prop's
1468
+ declared default, which the code leaves out as it does any default. A story
1469
+ that gives the prop leaves out the prop the component's own value starts
1470
+ from (`defaultOpen` beside `open`), which does nothing once the prop is
1471
+ given. A prop the story gives `null` is left out, unless it is a two-way
1472
+ prop the code binds; its state then starts at `null`, which type-checks
1473
+ where the template declares the prop with `null` (`type: 'string | null'`).
1474
+ Props print in the order the component declares them. The html is the markup
1475
+ at the story's value either way. It is printed in the page as the state
1476
+ changes, so it is always there for a story.
1477
+ - `html` - the markup the story renders to, at rest, as `storySource` prints
1478
+ it (see Using it from code): `{ status: 'ready', source, stylesheet?,
1479
+ warnings? }`, where `stylesheet` holds the styles the kit's build wrote for
1480
+ the components the markup renders, which the html pane shows it under, and
1481
+ `warnings` holds the evaluation warnings for the expressions the markup
1482
+ left out because they cannot be evaluated at rest, those one component
1483
+ gives for one reason as a single warning counting them, which the html
1484
+ pane counts in its legend and the html code pane beside its note, each
1485
+ count a button opening the warnings in a popover,
1486
+ `{ status: 'loading' }` while it is being printed, or
1487
+ `{ status: 'unavailable', reason }`, such as for a story holding a part
1488
+ that has no markup. The markup needs the kit's templates, so the page cannot
1489
+ print it: the development server prints it on request, once a change
1490
+ pauses, and drops a request the next change replaces. The static build
1491
+ prints the html of each scenario when it builds; for changed props or
1492
+ edited content it has none, and `html` says so.
1493
+
1494
+ `printStorySource(catalog, story)`, from the shell entry, prints any story's
1495
+ source from the catalog, for the story reference
1496
+ `frames.mountStory` takes, `{ component, scenario?, props? }`, so a shell can
1497
+ show the code beside a detached pane. It returns the source by target, for
1498
+ the targets other than html the component renders in, and throws for an unknown component
1499
+ or scenario. The html of a detached pane's story is not printed.
1500
+
1501
+ <!-- example: preview-print-story-source -->
1502
+ ```ts
1503
+ import { printStorySource } from '@scaffold-ui-kit/preview/shell';
1504
+
1505
+ const { react } = printStorySource(controller.catalog, { component: 'components-button', scenario: 'ghost' });
1506
+ ```
1507
+
1508
+ The catalog carries what the printing reads: `imports`, how a consumer
1509
+ imports each of the kit's components (`{ exportName, modules }`, the module by
1510
+ target), `svelteMode`, the mode the kit builds its Svelte target in
1511
+ (`legacy` or `runes`, the kit's `svelte.mode`), which the Svelte code holds a
1512
+ bound prop's state in, and on each component `previewDefaults`, the values
1513
+ the preview starts a prop from that the component does not declare, which
1514
+ the code leaves out, and `bindings`, its two-way props.
1515
+
1516
+ ### Frameworks
1517
+
1518
+ A shell may be written in React, Vue, Svelte or plain DOM. The preview's dev
1519
+ server compiles all of them, and a shell renders with the preview's own copy
1520
+ of each framework. While the dev server runs, an edit to the shell mounts the
1521
+ new shell over the same controller, keeping the selection and the editor's
1522
+ tree.
1523
+
1524
+ - **React.** `usePreview(controller, selector)` from
1525
+ `@scaffold-ui-kit/preview/react` reads a slice of the state and re-renders
1526
+ when it changes. Return a field of the state (or a primitive derived from
1527
+ one) from `selector`, not a new object. The preview compiles the shell
1528
+ without type-checking it; to type-check a React shell with `tsc`, the kit
1529
+ installs `@types/react` and `@types/react-dom`, and declares a module for
1530
+ stylesheets if the shell imports one:
1531
+
1532
+ ```ts
1533
+ // css.d.ts
1534
+ declare module '*.css';
1535
+ ```
1536
+
1537
+ - **Svelte.** `previewStore(controller, selector?)` from the shell entry is
1538
+ the state, or a slice of it, as a Svelte store, so a component reads it as
1539
+ `$preview`. Pass the controller through the `context` option of Svelte's
1540
+ `mount` and read it with `getContext`: it lives as long as the shell, and
1541
+ Svelte warns about a prop read once at the top of a component.
1542
+
1543
+ <!-- example: preview-svelte-shell -->
1544
+ ```ts
1545
+ import { mount, unmount } from 'svelte';
1546
+ import { defineShell } from '@scaffold-ui-kit/preview/shell';
1547
+ import Shell from './shell.svelte';
1548
+
1549
+ export default defineShell({
1550
+ apiVersion: 1,
1551
+ mount(root, controller) {
1552
+ const shell = mount(Shell, { target: root, context: new Map([['controller', controller]]) });
1553
+ return () => void unmount(shell);
1554
+ },
1555
+ });
1556
+ ```
1557
+
1558
+ Mount a pane in an attachment, or in an effect that reads nothing but the
1559
+ element and the framework. An effect that also reads the state re-runs on
1560
+ every change, unmounting the pane and mounting it afresh each time.
1561
+
1562
+ ```svelte
1563
+ <script lang="ts">
1564
+ import { getContext } from 'svelte';
1565
+ import { previewStore, type PreviewController } from '@scaffold-ui-kit/preview/shell';
1566
+
1567
+ const controller = getContext<PreviewController>('controller');
1568
+ const panes = previewStore(controller, (state) => state.targets.panes);
1569
+ </script>
1570
+
1571
+ {#each $panes as target (target)}
1572
+ <div class="pane" {@attach (element) => controller.frames.mount(element, target)}></div>
1573
+ {/each}
1574
+
1575
+ <style>
1576
+ .pane :global(iframe) { width: 100%; height: 20rem; border: 0; }
1577
+ </style>
1578
+ ```
1579
+
1580
+ A documentation page lists every scenario of the story in a detached pane
1581
+ of its own, with a field that sets one of its props:
1582
+
1583
+ ```svelte
1584
+ <script lang="ts">
1585
+ import { getContext } from 'svelte';
1586
+ import { previewStore, type PreviewController } from '@scaffold-ui-kit/preview/shell';
1587
+
1588
+ const controller = getContext<PreviewController>('controller');
1589
+ const preview = previewStore(controller);
1590
+ const component = $derived(controller.catalog.components.find((entry) => entry.id === $preview.selection.id));
1591
+ </script>
1592
+
1593
+ {#if component}
1594
+ <input
1595
+ value={String($preview.props.label ?? '')}
1596
+ oninput={(event) => controller.actions.setProps({ ...$preview.props, label: event.currentTarget.value })}
1597
+ />
1598
+ {#each component.scenarios.filter((scenario) => !scenario.empty) as scenario (`${component.id}:${scenario.id}`)}
1599
+ <figure>
1600
+ <figcaption>{scenario.label}</figcaption>
1601
+ <div
1602
+ class="pane"
1603
+ {@attach (element) => controller.frames.mountStory(element, component.targets[0], { component: component.id, scenario: scenario.id })}
1604
+ ></div>
1605
+ </figure>
1606
+ {/each}
1607
+ {/if}
1608
+ ```
1609
+
1610
+ - **Vue and plain DOM** subscribe to the controller directly, Vue into a
1611
+ `shallowRef` of the state.
1612
+
1613
+ ### A complete shell
1614
+
1615
+ A shell in plain DOM, with a select of stories, a checkbox per boolean option
1616
+ and the panes side by side:
1617
+
1618
+ <!-- example: preview-plain-shell -->
1619
+ ```ts
1620
+ import { defineShell, optionKind } from '@scaffold-ui-kit/preview/shell';
1621
+
1622
+ export default defineShell({
1623
+ apiVersion: 1,
1624
+ mount(root, controller) {
1625
+ const navigation = document.createElement('select');
1626
+ for (const component of controller.catalog.components.filter((component) => component.standalone && !component.unlisted)) {
1627
+ navigation.add(new Option(component.shortTitle, component.id));
1628
+ }
1629
+ navigation.onchange = () => controller.actions.selectComponent(navigation.value);
1630
+
1631
+ const toggles = controller.options.filter((option) => optionKind(option) === 'boolean').map((option) => {
1632
+ const box = Object.assign(document.createElement('input'), { type: 'checkbox', title: option.label });
1633
+ box.onchange = () => controller.actions.setOption(option.id, box.checked);
1634
+ return { option, box };
1635
+ });
1636
+
1637
+ const stage = document.createElement('div');
1638
+ stage.style.cssText = 'display: flex; height: 80vh';
1639
+ root.append(navigation, ...toggles.map(({ box }) => box), stage);
1640
+
1641
+ let panes = '';
1642
+ let unmountPanes: (() => void)[] = [];
1643
+ const render = (state = controller.getState()) => {
1644
+ navigation.value = state.selection.id;
1645
+ for (const { option, box } of toggles) box.checked = state.options[option.id] === true;
1646
+ if (state.targets.panes.join() === panes) return;
1647
+ panes = state.targets.panes.join();
1648
+ unmountPanes.forEach((unmount) => unmount());
1649
+ stage.replaceChildren();
1650
+ unmountPanes = state.targets.panes.map((target) => {
1651
+ const pane = stage.appendChild(document.createElement('div'));
1652
+ pane.style.flex = '1';
1653
+ const unmount = controller.frames.mount(pane, target);
1654
+ // The pane is a bare iframe: the shell sizes it.
1655
+ pane.querySelector('iframe')!.style.cssText = 'width: 100%; height: 100%; border: 0';
1656
+ return unmount;
1657
+ });
1658
+ };
1659
+ render();
1660
+ const stop = controller.subscribe(render);
1661
+ return () => {
1662
+ stop();
1663
+ unmountPanes.forEach((unmount) => unmount());
1664
+ };
1665
+ },
1666
+ });
1667
+ ```
1668
+
1669
+ `preview.shell` then names its module, such as `'preview/plain-shell.ts'`.
1670
+
1671
+ To set a story's props from such a shell, read them from `state.props` and
1672
+ hand back the whole set, here a checkbox per boolean prop:
1673
+
1674
+ <!-- example: preview-shell-props -->
1675
+ ```ts
1676
+ const component = controller.catalog.components.find((entry) => entry.id === state.selection.id);
1677
+ for (const propType of component?.propTypes.filter((entry) => entry.control === 'boolean' && entry.include) ?? []) {
1678
+ const box = Object.assign(document.createElement('input'), { type: 'checkbox', title: propType.declaredType });
1679
+ box.checked = state.props[propType.name] === true;
1680
+ box.onchange = () => controller.actions.setProps({ ...controller.getState().props, [propType.name]: box.checked });
1681
+ form.append(box);
1682
+ }
1683
+ ```
1684
+
1685
+ ## Commands
1686
+
1687
+ ```sh
1688
+ scaffold-ui-kit-preview dev # generate, then serve the preview
1689
+ scaffold-ui-kit-preview build # check, then build the static preview
1690
+ scaffold-ui-kit-preview generate # write the registries and manifest into .preview-generated/
1691
+ scaffold-ui-kit-preview check # check the story files, the manual specs and the build against the components
1692
+ ```
1693
+
1694
+ Every command takes `--kit <path>` for a kit root other than the current
1695
+ directory and `--configuration <path>`, relative to the directory the
1696
+ command runs in, for a kit configuration other than the one the kit gives
1697
+ (see Setting it up), `dev` takes `--port <number>` and `--host [address]` (every
1698
+ address when none is given, so another device on the network can open the
1699
+ preview), and `check --strict` fails on every warning the check itself
1700
+ raises too, such as a select value or boolean no scenario sets, a scenario
1701
+ prop set to `undefined` or a `standalone: false` story no compound lists
1702
+ (what reading the kit configuration and the kit's own build warn of stay
1703
+ warnings). `build` runs the check
1704
+ first, printing its warnings and errors as `check` does, and stops on
1705
+ an error, exiting non-zero. `build` warns of a bundled file larger than
1706
+ 500 kB after minification, naming it, except the host page's under the
1707
+ charcell shell, named as it is or configured by a module of the kit's,
1708
+ which holds the preview's interface and is that large whatever the kit
1709
+ holds. `.preview-generated/` (which also holds Vite's
1710
+ cache) and `.preview-static/` are generated and belong in `.gitignore`.
1711
+
1712
+ The static build serves from any file server, at a domain's root or under a
1713
+ subpath, such as a project site at `https://example.github.io/kit/`: its
1714
+ pages load everything by relative paths. The scenario editor and the Empty
1715
+ scenario need the dev server: each edit regenerates the story there. So does
1716
+ the html of a story whose props differ from its scenario's: the build prints
1717
+ the html of every scenario, one `html-source/<story id>.json` beside its
1718
+ pages, and the dev server prints any story's html on request. In the static
1719
+ build the html pane shows a scenario's printed html, and for changed props
1720
+ says it has none.
1721
+
1722
+ The dev server keeps the preview in step with the kit while it runs. Saving
1723
+ a story file, a template, a manual components module, a shared module, or a
1724
+ theme's stylesheet or assets regenerates the preview. Adding or deleting a
1725
+ template adds or removes its component, and a story file added or deleted is
1726
+ picked up the same way. Saves that arrive
1727
+ together regenerate once. Only the generated files whose content changed are
1728
+ written, and the pages that import them reload. If a module fails to load,
1729
+ for example because of a syntax error halfway through an edit, the server
1730
+ logs the error and keeps the last good preview until a save loads again. The
1731
+ views module, the setup modules and the kit's built components are served by
1732
+ Vite, which reloads them on its own. An edit to the kit configuration
1733
+ restarts the server with the new configuration. An edit to the shell mounts
1734
+ the new shell in place (see Frameworks).
1735
+
1736
+ A reload keeps whatever the address holds: the selected story or view, the
1737
+ scenario, the story's props, the visible targets and the option values.
1738
+ The scenario editor's tree is not: a reload seeds it afresh from the
1739
+ scenario and the tree built in it is lost, so a composition worth keeping
1740
+ belongs in a story file's scenario.
1741
+
1742
+ ## Using it from code
1743
+
1744
+ The package exports the pieces the commands are built from, for a kit's own
1745
+ scripts and tests. One is `storySource`, which prints a story as the code a
1746
+ consumer would write in each target, importing from the kit's package
1747
+ entries. A docs site can show it next to the component, and the snippets
1748
+ follow the story files. A default slot the story leaves unfilled is empty in
1749
+ every target, without the component's name the panes show there. A two-way prop the story gives is bound as the code
1750
+ view binds it (see A story's source):
1751
+
1752
+ <!-- example: preview-story-source -->
1753
+ ```ts
1754
+ import { storySource } from '@scaffold-ui-kit/preview';
1755
+
1756
+ const { react, vue, svelte, html } = await storySource(kitRoot, 'Button', { scenario: 'destructive' });
1757
+ ```
1758
+
1759
+ `html` is the markup the story renders to, for a reader who works without a
1760
+ framework: the template rendered once for the story's props and slot content,
1761
+ with the components it references rendered inline, and the exports of the
1762
+ kit's shared modules (the `add.shared` code modules its `package.json`
1763
+ exports), of `@scaffold-ui-kit/helpers/runtime` and `/zag` when the kit or
1764
+ one of its packages has that package installed, and the declarations on
1765
+ each template's own `imports` lines in scope for its expressions. A kit's
1766
+ own module wins a name it shares with the helpers. It is the markup at rest, with no behavior, and it assumes
1767
+ the kit's stylesheet. A component written by hand renders as
1768
+ `handWrittenComponents` maps it - its content, closed, its placeholder
1769
+ showing, or the template it renders - inside one
1770
+ `<!-- component: Name (behavior not included) -->` comment, and slot content
1771
+ written per framework renders its `html`. A story holding any other
1772
+ component without a template, or such slot content without `html`, gets no
1773
+ `html`, and a warning names what has no markup. An expression that cannot be
1774
+ evaluated without running the component is left out, so the part it decides
1775
+ shows at a default: a class it would add is missing, and a panel it would
1776
+ reveal stays out. These evaluation warnings, one per expression, are
1777
+ returned in `htmlWarnings` and printed once per story here and by
1778
+ `preview build`, where the expressions one component cannot evaluate for one
1779
+ reason print as one line counting them, such as
1780
+ `TabsTrigger: could not evaluate 3 expressions (isActive is not defined)`;
1781
+ a styling extension's notices are not among them. A call into a value a
1782
+ hand-written component exposes to its content, such as a driver's
1783
+ `api.getTriggerProps()`, has no result at rest by design and is left out
1784
+ without a warning. A name the component's
1785
+ script defines, such as its state, is one the markup cannot read; the
1786
+ helpers the template imports from a shared module (an `add.shared` code
1787
+ module the kit's `package.json` exports) or from `@scaffold-ui-kit/helpers`,
1788
+ or declares on its `imports` lines, are in scope, so an
1789
+ expression that should evaluate at rest calls one of those instead. Pass
1790
+ `targets` to print only some of them.
1791
+
1792
+ Each call prints one story on its own, so two examples on one page carry the
1793
+ same ids and radio `name`s: a label then points at the other example's
1794
+ field, and the radios of both form one group. Show each example in an iframe
1795
+ of its own.
1796
+
1797
+ ## Testing a kit's components
1798
+
1799
+ `@scaffold-ui-kit/preview/testing` drives the preview from a browser test;
1800
+ [`@scaffold-ui-kit/testing`](https://www.npmjs.com/package/@scaffold-ui-kit/testing)
1801
+ is built on it and sweeps every story with axe and the keyboard. It
1802
+ loads one target's preview page on its own, at the top level of the page,
1803
+ and mounts a story in it with the props and options you give, so locators and
1804
+ keyboard input reach the component directly:
1805
+
1806
+ <!-- example: preview-open-story -->
1807
+ ```ts
1808
+ import { openStory } from '@scaffold-ui-kit/preview/testing';
1809
+
1810
+ const story = await openStory(page, { target: 'vue', id: 'components-button', props: { variant: 'solid' } });
1811
+ await story.setProps({ variant: 'ghost' });
1812
+ await story.setOptions({ mode: 'dark' });
1813
+ await expect(story.page.getByRole('button')).toBeVisible();
1814
+ ```
1815
+
1816
+ A story's id is the one the catalog and the address use
1817
+ (`PreviewComponent.id`, `?component=` in a link): its title in lower case,
1818
+ each run of other characters a hyphen. A `Button` under the default
1819
+ `Components` category is `components-button`, and one a story file places in
1820
+ `Inputs` is `inputs-button`. An `unlisted` story opens this way too, so a
1821
+ story that exists for the tests can stay out of the navigation.
1822
+
1823
+ The same steps are available as functions of the page, for a suite that loads
1824
+ the page once per test and sets stories as it goes:
1825
+
1826
+ - `gotoPreview(page, target)` loads the target's preview page and waits
1827
+ until it listens. A page that never does, such as after an import fails to
1828
+ load, throws within seconds, naming the page and where to look.
1829
+ - `setStory(page, id, props)` shows a story, and `mountStory(page, id, props)`
1830
+ shows it from a fresh mount, with no focus or component state carried over
1831
+ from the story before. An id the page has no story for throws, listing
1832
+ those it has, as `openStory` does.
1833
+ - `waitForStory(page, id)` follows `mountStory` and waits until the story has
1834
+ finished mounting, whatever it rendered, including nothing, and throws after
1835
+ 15 seconds, naming the story. After `setStory` it can return on the page's
1836
+ earlier report of the same story.
1837
+ - `setProps(page, props)` replaces the story's props in place, keeping its
1838
+ component instance.
1839
+ - `setOptions(page, options)` applies option values, such as a color mode.
1840
+ An id the kit does not define throws, listing those it does, as does a
1841
+ value the option does not list.
1842
+ - `settle(page)` waits until the frame after the next paint, for a component
1843
+ whose event handling attaches just after it renders.
1844
+ - `readStoryIds(page)` lists the ids of the stories the page can mount.
1845
+ - `readRenderError(page)` is the message of what the mounted story threw as it
1846
+ rendered, as the pane reports it, or `undefined` while it renders.
1847
+ - `PREVIEW_MOUNT_ELEMENT_ID` is the id of the element the story mounts in,
1848
+ for a test that scopes its queries to the story.
1849
+ - `readPreviewManifest(kitRoot)` reads the components of
1850
+ `.preview-generated/manifest.json`, which the preview writes when it
1851
+ starts, typed `PreviewManifestComponent`.
1852
+
1853
+ `props` are the story's whole prop set: the story file's `defaultProps` and
1854
+ scenarios are not applied, so pass every prop the story needs, such as a
1855
+ required label.
1856
+
1857
+ `page` is a Playwright `Page`, or any object with `goto`, `waitForFunction`
1858
+ and `evaluate` of the same shape; the package does not depend on Playwright.
1859
+ Paths are relative, resolved against the test runner's base URL, or pass
1860
+ `baseUrl` to name the server. Serve the preview with `dev` or the static
1861
+ build.
1862
+
1863
+ ## Requirements
1864
+
1865
+ Node 22 or later.
1866
+
1867
+ ## License
1868
+
1869
+ MIT