@dfosco/storyboard 0.7.1 → 0.9.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 (323) hide show
  1. package/dist/storyboard-ui.css +1 -1
  2. package/dist/storyboard-ui.js +14128 -10953
  3. package/dist/storyboard-ui.js.map +1 -1
  4. package/dist/tailwind.css +1 -1
  5. package/dist/tiny-canvas.css +1 -1
  6. package/package.json +138 -128
  7. package/scaffold/AGENTS.md +9 -7
  8. package/scaffold/README.md +18 -0
  9. package/scaffold/deploy.yml +21 -0
  10. package/scaffold/gitignore +8 -14
  11. package/scaffold/preview.yml +40 -0
  12. package/scaffold/{manifest.json → scaffold.config.json} +73 -3
  13. package/scaffold/scripts/aggregate-artifacts.mjs +257 -0
  14. package/scaffold/skills/canvas/SKILL.md +1 -1
  15. package/scaffold/skills/migrate-0.5.0/SKILL.md +1 -1
  16. package/scaffold/skills/ship/SKILL.md +2 -2
  17. package/scaffold/skills/worktree/SKILL.md +1 -1
  18. package/scaffold/src/_prototype.jsx +17 -0
  19. package/scaffold/src/_story.jsx +13 -0
  20. package/scaffold/src/library/README.md +34 -0
  21. package/scaffold/src/library/_app.jsx +41 -0
  22. package/scaffold/src/library/_app.module.css +2 -0
  23. package/scaffold/src/library/create.jsx +10 -0
  24. package/scaffold/src/library/index.jsx +14 -0
  25. package/scaffold/src/library/mount.jsx +51 -0
  26. package/scaffold/src/library/prototypes-entry.jsx +57 -0
  27. package/scaffold/src/library/routes.jsx +205 -0
  28. package/scaffold/src/library/viewfinder.jsx +13 -0
  29. package/scaffold/src/library/workspace.jsx +17 -0
  30. package/scripts/aggregate-artifacts.mjs +257 -0
  31. package/src/canvas/style.css +16 -16
  32. package/src/core/artifact/operations.js +54 -15
  33. package/src/core/artifact/schemas/canvas.schema.json +7 -1
  34. package/src/core/autosync/server.test.js +1 -1
  35. package/src/core/canvas/hot-pool.js +21 -33
  36. package/src/core/canvas/materializer.js +39 -13
  37. package/src/core/canvas/selectedWidgets.js +1 -1
  38. package/src/core/canvas/server.js +376 -100
  39. package/src/core/canvas/server.test.js +472 -2
  40. package/src/core/canvas/terminal-server.js +144 -57
  41. package/src/core/canvas/undoRedo.js +174 -0
  42. package/src/core/canvas/undoRedo.test.js +250 -0
  43. package/src/core/cli/artifact.js +2 -0
  44. package/src/core/cli/buildManifest.js +34 -0
  45. package/src/core/cli/canvasRedo.js +44 -0
  46. package/src/core/cli/canvasUndo.js +40 -0
  47. package/src/core/cli/create.js +151 -23
  48. package/src/core/cli/dev.js +23 -35
  49. package/src/core/cli/index.js +21 -0
  50. package/src/core/cli/schemas.js +8 -4
  51. package/src/core/cli/serverUrl.js +24 -0
  52. package/src/core/cli/setup.js +14 -25
  53. package/src/core/cli/terminal-welcome.js +12 -6
  54. package/src/core/cli/updateVersion.js +42 -2
  55. package/src/core/cli/workleaf.js +140 -0
  56. package/src/core/comments/api.js +1 -1
  57. package/src/core/comments/queries.js +3 -0
  58. package/src/core/comments/ui/CommentWindow.jsx +3 -2
  59. package/src/core/comments/ui/comment-layout.css +14 -14
  60. package/src/core/data/artifactIndex.js +235 -0
  61. package/src/core/data/artifactIndex.test.js +203 -0
  62. package/src/core/data/artifactManifest.js +290 -0
  63. package/src/core/data/artifactManifest.test.js +207 -0
  64. package/src/core/data/viewfinder.js +47 -7
  65. package/src/core/index.js +24 -1
  66. package/src/core/inspector/highlighter.js +1 -1
  67. package/src/core/inspector/mouseMode.js +2 -2
  68. package/src/core/lib/components/ui/avatar/avatar-fallback.jsx +2 -2
  69. package/src/core/lib/components/ui/avatar/avatar-image.jsx +2 -2
  70. package/src/core/lib/components/ui/avatar/avatar.jsx +2 -2
  71. package/src/core/lib/components/ui/checkbox/checkbox.jsx +12 -9
  72. package/src/core/lib/components/ui/dialog/dialog-content.jsx +45 -31
  73. package/src/core/lib/components/ui/dialog/dialog-trigger.jsx +9 -5
  74. package/src/core/lib/components/ui/dialog/dialog.jsx +12 -3
  75. package/src/core/lib/components/ui/dialog/dialog.module.css +118 -0
  76. package/src/core/lib/components/ui/dialog/index.js +4 -34
  77. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +3 -3
  78. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +19 -9
  79. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +8 -8
  80. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +2 -2
  81. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +2 -2
  82. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +15 -4
  83. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +2 -2
  84. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +2 -2
  85. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +14 -7
  86. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +2 -2
  87. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +8 -4
  88. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +4 -4
  89. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +2 -2
  90. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +8 -2
  91. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu.jsx +2 -2
  92. package/src/core/lib/components/ui/label/label.jsx +1 -2
  93. package/src/core/lib/components/ui/panel/panel-close.jsx +7 -4
  94. package/src/core/lib/components/ui/panel/panel-content.jsx +7 -9
  95. package/src/core/lib/components/ui/panel/panel-title.jsx +3 -3
  96. package/src/core/lib/components/ui/panel/panel.jsx +11 -2
  97. package/src/core/lib/components/ui/separator/separator.jsx +6 -4
  98. package/src/core/lib/components/ui/tooltip/tooltip-content.jsx +7 -6
  99. package/src/core/lib/components/ui/tooltip/tooltip-trigger.jsx +13 -6
  100. package/src/core/lib/components/ui/tooltip/tooltip.jsx +5 -5
  101. package/src/core/modes/modes.css +4 -4
  102. package/src/core/mountStoryboardCore.js +287 -83
  103. package/src/core/mountStoryboardCore.test.js +198 -0
  104. package/src/core/rename-watcher/config.json +1 -1
  105. package/src/core/rename-watcher/watcher.js +330 -37
  106. package/src/core/rename-watcher/watcher.test.js +253 -0
  107. package/src/core/scaffold/fragments.js +287 -0
  108. package/src/core/scaffold/fragments.test.js +401 -0
  109. package/src/core/scaffold/index.js +291 -0
  110. package/src/core/scaffold/migrateLegacyGitignore.js +157 -0
  111. package/src/core/scaffold/scaffold-integration.test.js +247 -0
  112. package/src/core/scaffold.js +4 -97
  113. package/src/core/stores/canvasInteractionStore.js +120 -0
  114. package/src/core/stores/canvasInteractionStore.test.js +126 -0
  115. package/src/core/stores/configSchema.js +64 -8
  116. package/src/core/stores/configSchema.test.js +3 -0
  117. package/src/core/stores/customerModeConfig.js +24 -3
  118. package/src/core/stores/customerModeConfig.test.js +46 -0
  119. package/src/core/stores/presentationStore.js +118 -0
  120. package/src/core/stores/presentationStore.test.js +114 -0
  121. package/src/core/stores/recentArtifacts.js +49 -2
  122. package/src/core/stores/recentArtifacts.test.js +59 -6
  123. package/src/core/stores/themeBootstrap.js +264 -0
  124. package/src/core/stores/themeBootstrap.test.js +211 -0
  125. package/src/core/stores/themeStore.test.js +154 -0
  126. package/src/core/stores/themeStore.ts +235 -220
  127. package/src/core/stores/uiConfig.js +10 -2
  128. package/src/core/styles/generateThemesCss.js +63 -0
  129. package/src/core/styles/tailwind.css +150 -94
  130. package/src/core/styles/workspace-chrome.css +13 -0
  131. package/src/core/tools/handlers/paletteTheme.js +11 -8
  132. package/src/core/ui/ActionMenuButton.jsx +2 -1
  133. package/src/core/ui/AutosyncMenuButton.css +7 -7
  134. package/src/core/ui/BranchSelect.module.css +5 -5
  135. package/src/core/ui/CanvasCreateMenu.jsx +1 -19
  136. package/src/core/ui/ChromeSlot.jsx +92 -0
  137. package/src/core/ui/ChromeSlot.test.jsx +140 -0
  138. package/src/core/ui/CoreUIBar.css +5 -5
  139. package/src/core/ui/CoreUIBar.jsx +170 -142
  140. package/src/core/ui/InspectorPanel.css +5 -5
  141. package/src/core/ui/InspectorPanel.jsx +9 -9
  142. package/src/core/ui/PwaInstallBanner.css +5 -5
  143. package/src/core/ui/PwaInstallBanner.jsx +5 -5
  144. package/src/core/ui/ThemeMenuButton.jsx +45 -30
  145. package/src/core/ui/core-ui-colors.css +28 -66
  146. package/src/core/ui/sidepanel.css +12 -12
  147. package/src/core/vite/server-plugin.js +13 -0
  148. package/src/core/workleaf/cleanup.js +102 -0
  149. package/src/core/workleaf/create.js +199 -0
  150. package/src/core/workleaf/delete.js +101 -0
  151. package/src/core/workleaf/list.js +57 -0
  152. package/src/core/workleaf/merge.js +238 -0
  153. package/src/core/workleaf/paths.js +146 -0
  154. package/src/core/workleaf/paths.test.js +230 -0
  155. package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +130 -34
  156. package/src/core/workshop/features/createFlow/server.js +5 -1
  157. package/src/core/workshop/features/createPrototype/server.js +5 -1
  158. package/src/core/workshop/features/templateIndex.js +28 -1
  159. package/src/core/worktree/port.js +48 -5
  160. package/src/core/worktree/port.test.js +42 -0
  161. package/src/internals/ArtifactForm/ArtifactForm.jsx +173 -112
  162. package/src/internals/ArtifactForm/ArtifactForm.module.css +51 -69
  163. package/src/internals/ArtifactForm/artifactSchemas.js +40 -5
  164. package/src/internals/AuthModal/AuthModal.module.css +34 -34
  165. package/src/internals/BaseUiForm/BaseUiForm.jsx +293 -0
  166. package/src/internals/BaseUiForm/BaseUiForm.module.css +542 -0
  167. package/src/internals/BranchBar/BranchBar.module.css +4 -4
  168. package/src/internals/BranchesDropdown/BranchesDropdown.jsx +69 -0
  169. package/src/internals/BranchesDropdown/BranchesDropdown.module.css +84 -0
  170. package/src/internals/CommandPalette/CommandPalette.jsx +41 -29
  171. package/src/internals/CommandPalette/CreateArtifactForm.jsx +150 -0
  172. package/src/internals/CommandPalette/CreateDialog.jsx +20 -110
  173. package/src/internals/CommandPalette/WidgetArtifactDialog.css +16 -16
  174. package/src/internals/CommandPalette/WidgetArtifactDialog.jsx +1 -1
  175. package/src/internals/CommandPalette/command-palette.css +14 -14
  176. package/src/internals/CommandPalette/createHelpers.js +30 -0
  177. package/src/internals/CreatePage/CreatePage.jsx +57 -0
  178. package/src/internals/CreatePage/CreatePage.module.css +34 -0
  179. package/src/internals/Dialog.jsx +51 -0
  180. package/src/internals/FlowError.module.css +5 -5
  181. package/src/internals/PrototypeErrorBoundary.module.css +15 -15
  182. package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +534 -0
  183. package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +323 -0
  184. package/src/internals/Tooltip.jsx +39 -0
  185. package/src/internals/Viewfinder.jsx +229 -64
  186. package/src/internals/Viewfinder.module.css +253 -182
  187. package/src/internals/canvas/CanvasControls.module.css +11 -11
  188. package/src/internals/canvas/CanvasPage.bridge.test.jsx +11 -37
  189. package/src/internals/canvas/CanvasPage.jsx +358 -217
  190. package/src/internals/canvas/CanvasPage.module.css +20 -19
  191. package/src/internals/canvas/CanvasPage.multiselect.test.jsx +39 -26
  192. package/src/internals/canvas/CanvasToolbar.module.css +11 -11
  193. package/src/internals/canvas/ConnectorLayer.jsx +39 -2
  194. package/src/internals/canvas/ConnectorLayer.module.css +10 -10
  195. package/src/internals/canvas/PageSelector.jsx +15 -1
  196. package/src/internals/canvas/PageSelector.module.css +41 -27
  197. package/src/internals/canvas/canvasApi.js +40 -5
  198. package/src/internals/canvas/canvasTheme.js +22 -111
  199. package/src/internals/canvas/isolation/createIsolationMiddleware.js +68 -0
  200. package/src/internals/canvas/isolation/mountIsolationApp.jsx +88 -0
  201. package/src/internals/canvas/isolation/prototypeRoutes.jsx +140 -0
  202. package/src/internals/canvas/isolation/prototypesEntry.jsx +28 -0
  203. package/src/internals/canvas/isolation/storiesEntry.jsx +20 -0
  204. package/src/internals/canvas/isolation/storyRoutes.jsx +55 -0
  205. package/src/internals/canvas/useUndoRedo.js +75 -60
  206. package/src/internals/canvas/useUndoRedo.test.js +46 -178
  207. package/src/internals/canvas/widgets/CodePenEmbed.jsx +16 -36
  208. package/src/internals/canvas/widgets/CodePenEmbed.module.css +12 -12
  209. package/src/internals/canvas/widgets/CropOverlay.module.css +23 -23
  210. package/src/internals/canvas/widgets/ExpandedPane.module.css +8 -8
  211. package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +1 -1
  212. package/src/internals/canvas/widgets/FigmaEmbed.jsx +15 -36
  213. package/src/internals/canvas/widgets/FigmaEmbed.module.css +15 -15
  214. package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +30 -21
  215. package/src/internals/canvas/widgets/FrozenTerminalOverlay.module.css +5 -5
  216. package/src/internals/canvas/widgets/ImageWidget.jsx +7 -2
  217. package/src/internals/canvas/widgets/ImageWidget.module.css +3 -3
  218. package/src/internals/canvas/widgets/LinkPreview.module.css +79 -80
  219. package/src/internals/canvas/widgets/MarkdownBlock.module.css +31 -31
  220. package/src/internals/canvas/widgets/PromptWidget.jsx +2 -16
  221. package/src/internals/canvas/widgets/PromptWidget.module.css +33 -33
  222. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +92 -17
  223. package/src/internals/canvas/widgets/PrototypeEmbed.module.css +58 -56
  224. package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +118 -4
  225. package/src/internals/canvas/widgets/ResizeHandle.module.css +4 -4
  226. package/src/internals/canvas/widgets/StorySetWidget.jsx +53 -25
  227. package/src/internals/canvas/widgets/StoryWidget.jsx +71 -56
  228. package/src/internals/canvas/widgets/StoryWidget.module.css +22 -20
  229. package/src/internals/canvas/widgets/TerminalReadWidget.jsx +13 -15
  230. package/src/internals/canvas/widgets/TerminalReadWidget.module.css +4 -4
  231. package/src/internals/canvas/widgets/TerminalWidget.jsx +56 -45
  232. package/src/internals/canvas/widgets/TerminalWidget.module.css +10 -10
  233. package/src/internals/canvas/widgets/TilesWidget.module.css +17 -17
  234. package/src/internals/canvas/widgets/WidgetChrome.jsx +1 -1
  235. package/src/internals/canvas/widgets/WidgetChrome.module.css +76 -61
  236. package/src/internals/canvas/widgets/WidgetWrapper.module.css +2 -2
  237. package/src/internals/canvas/widgets/embedOverlay.module.css +14 -4
  238. package/src/internals/canvas/widgets/embedTheme.js +12 -104
  239. package/src/internals/canvas/widgets/expandUtils.js +19 -19
  240. package/src/internals/canvas/widgets/normalizeLegacyEmbedSrc.js +74 -0
  241. package/src/internals/canvas/widgets/widgetConfig.js +5 -5
  242. package/src/internals/context.jsx +22 -1
  243. package/src/internals/context.test.jsx +47 -0
  244. package/src/internals/hooks/useArtifactIndex.js +31 -0
  245. package/src/internals/hooks/useArtifactIndex.test.js +45 -0
  246. package/src/internals/hooks/usePrototypeReloadGuard.js +14 -8
  247. package/src/internals/index.js +6 -0
  248. package/src/internals/loadConsumerWrapper.js +60 -0
  249. package/src/internals/story/ComponentSetPage.jsx +75 -34
  250. package/src/internals/story/ComponentSetPage.module.css +14 -14
  251. package/src/internals/story/StoryPage.jsx +16 -25
  252. package/src/internals/story/StoryPage.module.css +2 -2
  253. package/src/internals/vite/data-plugin.js +317 -149
  254. package/src/internals/vite/data-plugin.test.js +11 -11
  255. package/{scaffold/storyboard.config.json → storyboard.config.json} +25 -0
  256. package/terminal.config.json +1 -1
  257. package/src/core/lib/components/ui/card/card-action.jsx +0 -11
  258. package/src/core/lib/components/ui/card/card-content.jsx +0 -11
  259. package/src/core/lib/components/ui/card/card-description.jsx +0 -11
  260. package/src/core/lib/components/ui/card/card-footer.jsx +0 -11
  261. package/src/core/lib/components/ui/card/card-header.jsx +0 -19
  262. package/src/core/lib/components/ui/card/card-title.jsx +0 -11
  263. package/src/core/lib/components/ui/card/card.jsx +0 -17
  264. package/src/core/lib/components/ui/card/index.js +0 -25
  265. package/src/core/lib/components/ui/collapsible/collapsible-content.jsx +0 -7
  266. package/src/core/lib/components/ui/collapsible/collapsible-trigger.jsx +0 -7
  267. package/src/core/lib/components/ui/collapsible/collapsible.jsx +0 -7
  268. package/src/core/lib/components/ui/collapsible/index.js +0 -13
  269. package/src/core/lib/components/ui/dialog/dialog-close.jsx +0 -7
  270. package/src/core/lib/components/ui/dialog/dialog-description.jsx +0 -15
  271. package/src/core/lib/components/ui/dialog/dialog-footer.jsx +0 -23
  272. package/src/core/lib/components/ui/dialog/dialog-header.jsx +0 -11
  273. package/src/core/lib/components/ui/dialog/dialog-overlay.jsx +0 -15
  274. package/src/core/lib/components/ui/dialog/dialog-portal.jsx +0 -4
  275. package/src/core/lib/components/ui/dialog/dialog-title.jsx +0 -15
  276. package/src/core/lib/components/ui/popover/index.js +0 -28
  277. package/src/core/lib/components/ui/popover/popover-close.jsx +0 -7
  278. package/src/core/lib/components/ui/popover/popover-content.jsx +0 -22
  279. package/src/core/lib/components/ui/popover/popover-description.jsx +0 -11
  280. package/src/core/lib/components/ui/popover/popover-header.jsx +0 -11
  281. package/src/core/lib/components/ui/popover/popover-portal.jsx +0 -4
  282. package/src/core/lib/components/ui/popover/popover-title.jsx +0 -11
  283. package/src/core/lib/components/ui/popover/popover-trigger.jsx +0 -8
  284. package/src/core/lib/components/ui/popover/popover.jsx +0 -4
  285. package/src/core/lib/components/ui/select/index.js +0 -37
  286. package/src/core/lib/components/ui/select/select-content.jsx +0 -30
  287. package/src/core/lib/components/ui/select/select-group-heading.jsx +0 -17
  288. package/src/core/lib/components/ui/select/select-group.jsx +0 -15
  289. package/src/core/lib/components/ui/select/select-item.jsx +0 -26
  290. package/src/core/lib/components/ui/select/select-label.jsx +0 -11
  291. package/src/core/lib/components/ui/select/select-portal.jsx +0 -4
  292. package/src/core/lib/components/ui/select/select-scroll-down-button.jsx +0 -18
  293. package/src/core/lib/components/ui/select/select-scroll-up-button.jsx +0 -18
  294. package/src/core/lib/components/ui/select/select-separator.jsx +0 -15
  295. package/src/core/lib/components/ui/select/select-trigger.jsx +0 -25
  296. package/src/core/lib/components/ui/select/select.jsx +0 -4
  297. package/src/core/lib/components/ui/sheet/index.js +0 -34
  298. package/src/core/lib/components/ui/sheet/sheet-close.jsx +0 -7
  299. package/src/core/lib/components/ui/sheet/sheet-content.jsx +0 -35
  300. package/src/core/lib/components/ui/sheet/sheet-description.jsx +0 -15
  301. package/src/core/lib/components/ui/sheet/sheet-footer.jsx +0 -11
  302. package/src/core/lib/components/ui/sheet/sheet-header.jsx +0 -11
  303. package/src/core/lib/components/ui/sheet/sheet-overlay.jsx +0 -15
  304. package/src/core/lib/components/ui/sheet/sheet-portal.jsx +0 -4
  305. package/src/core/lib/components/ui/sheet/sheet-title.jsx +0 -15
  306. package/src/core/lib/components/ui/sheet/sheet-trigger.jsx +0 -7
  307. package/src/core/lib/components/ui/sheet/sheet.jsx +0 -4
  308. package/src/core/lib/components/ui/toggle/index.js +0 -8
  309. package/src/core/lib/components/ui/toggle/toggle.jsx +0 -36
  310. package/src/core/lib/components/ui/toggle-group/index.js +0 -10
  311. package/src/core/lib/components/ui/toggle-group/toggle-group-item.jsx +0 -29
  312. package/src/core/lib/components/ui/toggle-group/toggle-group.jsx +0 -43
  313. package/src/internals/canvas/StoryErrorBoundary.jsx +0 -47
  314. package/src/internals/canvas/WebGLContextPool.jsx +0 -291
  315. package/src/internals/canvas/WebGLContextPool.test.jsx +0 -165
  316. package/src/internals/canvas/componentIsolate.jsx +0 -144
  317. package/src/internals/canvas/componentSetIsolate.jsx +0 -300
  318. package/src/internals/canvas/widgets/ComponentSetWidget.module.css +0 -89
  319. package/src/internals/canvas/widgets/InlineStoryRenderer.jsx +0 -129
  320. package/src/internals/canvas/widgets/InlineStoryRenderer.test.jsx +0 -68
  321. package/src/internals/canvas/widgets/StorySetWidget.module.css +0 -89
  322. package/src/primer/ThemeSync.jsx +0 -73
  323. package/src/primer/index.js +0 -1
@@ -21,6 +21,7 @@ import {
21
21
  buildTemplateRecipeIndex,
22
22
  resolveTemplateRecipeEntry,
23
23
  } from '../templateIndex.js'
24
+ import { readWorkshopPartials } from '../partialRender.js'
24
25
 
25
26
  const FLOW_SKELETON = JSON.stringify({ $global: [] }, null, 2) + '\n'
26
27
 
@@ -178,7 +179,10 @@ function generatePrototypeJson({ title, author, description, partialEntry, url }
178
179
  */
179
180
  export function createPrototypesHandler(ctx) {
180
181
  const { root, sendJson, workshopConfig = {} } = ctx
181
- const getTemplateRecipes = () => buildTemplateRecipeIndex(root, workshopConfig.partials)
182
+ const getTemplateRecipes = () => {
183
+ const partials = readWorkshopPartials(root)
184
+ return buildTemplateRecipeIndex(root, partials.length ? partials : workshopConfig.partials)
185
+ }
182
186
 
183
187
  return async (req, res, { body, path: routePath, method }) => {
184
188
  const templateRecipes = getTemplateRecipes()
@@ -70,6 +70,26 @@ function makeId(prefix, directory, name, prototype, folder) {
70
70
  return `prototype:${folderPart}${prototype}:${directory}/${name}`
71
71
  }
72
72
 
73
+ /**
74
+ * Resolve the actual filesystem directory for a global partial. The
75
+ * config says `directory: "template"` (singular) but the filesystem may
76
+ * use `templates` (plural). Probe both, return whichever exists. Returns
77
+ * `null` when neither directory contains the named partial.
78
+ */
79
+ function resolveGlobalPartialBaseDir(srcRoot, configDirectory, name) {
80
+ const kind = toPartialKind(configDirectory)
81
+ if (!kind) return null
82
+ // Both spellings are accepted in config; the actual on-disk dir may
83
+ // differ. Try the config spelling first, then the alternative.
84
+ const alternatives = kind === 'template'
85
+ ? Array.from(new Set([configDirectory, 'templates', 'template']))
86
+ : Array.from(new Set([configDirectory, 'recipes', 'recipe']))
87
+ for (const dir of alternatives) {
88
+ if (fs.existsSync(path.join(srcRoot, dir, name))) return dir
89
+ }
90
+ return null
91
+ }
92
+
73
93
  export function buildTemplateRecipeIndex(root, configPartials = []) {
74
94
  const srcRoot = path.join(root, 'src')
75
95
  const results = []
@@ -82,13 +102,20 @@ export function buildTemplateRecipeIndex(root, configPartials = []) {
82
102
  const id = makeId('global', partial.directory, partial.name)
83
103
  if (seenIds.has(id)) continue
84
104
  seenIds.add(id)
105
+ // Resolve baseDir to the directory that actually exists on disk so
106
+ // downstream code (operations.js createPrototype) reads the right
107
+ // path. Without this, a config with `directory: "template"` while
108
+ // the files live in `src/templates/` would silently fail at write
109
+ // time and leave an orphaned .prototype.json behind.
110
+ const resolvedBaseDir = resolveGlobalPartialBaseDir(srcRoot, partial.directory, partial.name)
111
+ || partial.directory
85
112
  results.push({
86
113
  id,
87
114
  name: partial.name,
88
115
  directory: partial.directory,
89
116
  kind,
90
117
  scope: 'global',
91
- baseDir: partial.directory,
118
+ baseDir: resolvedBaseDir,
92
119
  globals: Array.isArray(partial.globals) ? partial.globals : undefined,
93
120
  })
94
121
  }
@@ -30,7 +30,22 @@ const BASE_PORT = 1234
30
30
  * @returns {string} absolute path to ports.json
31
31
  */
32
32
  export function portsFilePath(cwd = process.cwd()) {
33
- const realCwd = realpathSync(cwd)
33
+ // Defensive: cwd may have just been deleted out from under us (e.g. a
34
+ // workleaf merge cleaning up its own directory before releasing the
35
+ // port). Fall back to the raw path so callers don't ENOENT.
36
+ let realCwd
37
+ try {
38
+ realCwd = realpathSync(cwd)
39
+ } catch {
40
+ realCwd = cwd
41
+ }
42
+
43
+ // Check workleaf first — its path is `<root>/workleafs/<parent>/workleaf--<slug>`
44
+ // so the prefix capture must NOT swallow that segment when we're inside one.
45
+ const workleafMatch = realCwd.match(/^(.+)[/\\]workleafs[/\\][^/\\]+[/\\]workleaf--[^/\\]+/)
46
+ if (workleafMatch) {
47
+ return join(workleafMatch[1], 'worktrees', 'ports.json')
48
+ }
34
49
 
35
50
  // Check if we're inside worktrees/<name>/ or .worktrees/<name>/
36
51
  const worktreeMatch = realCwd.match(/^(.+)[/\\]\.?worktrees[/\\][^/\\]+/)
@@ -45,20 +60,35 @@ export function portsFilePath(cwd = process.cwd()) {
45
60
  /**
46
61
  * Detect the worktree name from the current git context.
47
62
  *
48
- * Returns 'main' when not inside a worktrees/<name>/ or
49
- * .worktrees/<name>/ directory.
63
+ * Returns:
64
+ * - The compound branch name `<parent>--workleaf--<slug>` when cwd
65
+ * is inside a `workleafs/<parent>/workleaf--<slug>/` directory.
66
+ * This keeps `storyboard dev` (and every consumer that calls
67
+ * this helper) working from inside a workleaf with zero changes.
68
+ * - The worktree name when cwd is inside `worktrees/<name>/` or
69
+ * `.worktrees/<name>/`.
70
+ * - The current branch name when on a non-main/master branch.
71
+ * - `'main'` otherwise.
50
72
  */
51
73
  export function detectWorktreeName() {
52
74
  try {
53
75
  const topLevel = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim()
54
76
  const realTop = realpathSync(topLevel)
55
77
 
78
+ // Check workleaf first — `workleafs/<parent>/workleaf--<slug>` is
79
+ // the most specific pattern and overlaps with the worktree match
80
+ // when the parent is itself a worktree.
81
+ const workleafMatch = realTop.match(/[/\\]workleafs[/\\]([^/\\]+)[/\\]workleaf--([^/\\]+)$/)
82
+ if (workleafMatch) return `${workleafMatch[1]}--workleaf--${workleafMatch[2]}`
83
+
56
84
  // Check if we're inside a worktrees/<name> or .worktrees/<name> directory
57
85
  const wtMatch = realTop.match(/[/\\]\.?worktrees[/\\]([^/\\]+)$/)
58
86
  if (wtMatch) return wtMatch[1]
59
87
 
60
- // Also check the cwd pattern
88
+ // Also check the cwd pattern (workleaf first, then worktree)
61
89
  const realCwd = realpathSync(process.cwd())
90
+ const cwdWorkleafMatch = realCwd.match(/[/\\]workleafs[/\\]([^/\\]+)[/\\]workleaf--([^/\\]+)/)
91
+ if (cwdWorkleafMatch) return `${cwdWorkleafMatch[1]}--workleaf--${cwdWorkleafMatch[2]}`
62
92
  const worktreeMatch = realCwd.match(/\.?worktrees[/\\]([^/\\]+)/)
63
93
  if (worktreeMatch) return worktreeMatch[1]
64
94
 
@@ -226,7 +256,20 @@ export function slugify(name) {
226
256
  * @returns {string} absolute path to repo root
227
257
  */
228
258
  export function repoRoot(cwd = process.cwd()) {
229
- const realCwd = realpathSync(cwd)
259
+ // Defensive: cwd may have been deleted (e.g. during workleaf cleanup).
260
+ // Fall back to raw cwd so the regex matchers still get a useful path.
261
+ let realCwd
262
+ try {
263
+ realCwd = realpathSync(cwd)
264
+ } catch {
265
+ realCwd = cwd
266
+ }
267
+
268
+ // Workleaf wins over worktree: `workleafs/<parent>/workleaf--<slug>`
269
+ // appears below the parent worktree on disk, but the repo root is
270
+ // always the directory containing the top-level `workleafs/` dir.
271
+ const workleafMatch = realCwd.match(/^(.+)[/\\]workleafs[/\\][^/\\]+[/\\]workleaf--[^/\\]+/)
272
+ if (workleafMatch) return workleafMatch[1]
230
273
 
231
274
  const worktreeMatch = realCwd.match(/^(.+)[/\\]\.?worktrees[/\\][^/\\]+/)
232
275
  if (worktreeMatch) return worktreeMatch[1]
@@ -164,6 +164,48 @@ describe('repoRoot', () => {
164
164
  const wt = join(tempRoot, '.worktrees', 'legacy-branch')
165
165
  expect(repoRoot(wt)).toBe(tempRoot)
166
166
  })
167
+
168
+ it('returns parent of workleafs when inside a workleaf (main-parented)', () => {
169
+ const wl = join(tempRoot, 'workleafs', 'main', 'workleaf--fix')
170
+ mkdirSync(wl, { recursive: true })
171
+ expect(repoRoot(wl)).toBe(tempRoot)
172
+ })
173
+
174
+ it('returns parent of workleafs when inside a workleaf (worktree-parented)', () => {
175
+ const wl = join(tempRoot, 'workleafs', '4.3.0', 'workleaf--bench')
176
+ mkdirSync(wl, { recursive: true })
177
+ expect(repoRoot(wl)).toBe(tempRoot)
178
+ })
179
+
180
+ it('returns parent of workleafs even from a nested cwd inside a workleaf', () => {
181
+ const wl = join(tempRoot, 'workleafs', 'main', 'workleaf--fix', 'src', 'core')
182
+ mkdirSync(wl, { recursive: true })
183
+ expect(repoRoot(wl)).toBe(tempRoot)
184
+ })
185
+ })
186
+
187
+ describe('portsFilePath (workleaf-aware)', () => {
188
+ let tempRoot
189
+
190
+ beforeEach(() => {
191
+ tempRoot = realpathSync(mkdtempSync(join(tmpdir(), 'sb-port-wl-')))
192
+ })
193
+
194
+ afterEach(() => {
195
+ rmSync(tempRoot, { recursive: true, force: true })
196
+ })
197
+
198
+ it('returns root worktrees/ports.json from inside a workleaf', () => {
199
+ const wl = join(tempRoot, 'workleafs', 'main', 'workleaf--fix')
200
+ mkdirSync(wl, { recursive: true })
201
+ expect(portsFilePath(wl)).toBe(join(tempRoot, 'worktrees', 'ports.json'))
202
+ })
203
+
204
+ it('returns root worktrees/ports.json from inside a worktree-parented workleaf', () => {
205
+ const wl = join(tempRoot, 'workleafs', '4.3.0', 'workleaf--bench')
206
+ mkdirSync(wl, { recursive: true })
207
+ expect(portsFilePath(wl)).toBe(join(tempRoot, 'worktrees', 'ports.json'))
208
+ })
167
209
  })
168
210
 
169
211
  describe('worktreeDir', () => {
@@ -3,6 +3,11 @@
3
3
  * submit actions for any artifact type. Surfaces identically across the
4
4
  * command palette, canvas add menu, and workspace.
5
5
  *
6
+ * Styling: the form is built on the BaseUiForm primitives (`@base-ui/react`
7
+ * + workspace-style CSS) — Mona Sans, pill buttons, clean white cards.
8
+ * This replaced the prior Primer-based implementation so artifact creation
9
+ * shares the SimpleWorkspace look across every surface.
10
+ *
6
11
  * Modes:
7
12
  * <ArtifactForm type="prototype" onSubmit={fn} /> // single type
8
13
  * <ArtifactForm onSubmit={fn} /> // type picker
@@ -11,32 +16,64 @@
11
16
  * Schemas may declare `field.dynamic = 'prototypes'`. Pass
12
17
  * `dynamicOptions={{ prototypes: ['my-app', ...] }}` to populate them.
13
18
  */
14
- import { useState, useMemo, useEffect } from 'react'
15
- import { FormControl, Button, TextInput, Textarea, Flash, Text, ActionMenu, ActionList } from '@primer/react'
16
- import { ARTIFACT_SCHEMAS, validateArtifact } from './artifactSchemas.js'
19
+ import { useState, useMemo, useEffect, useRef } from 'react'
20
+ import {
21
+ Form,
22
+ Field,
23
+ Label,
24
+ Description,
25
+ Validation,
26
+ TextInput,
27
+ Textarea,
28
+ Select,
29
+ Checkbox,
30
+ Button,
31
+ Flash,
32
+ Menu,
33
+ } from '../BaseUiForm/BaseUiForm.jsx'
34
+ import { ARTIFACT_SCHEMAS, validateArtifact, isFieldVisible } from './artifactSchemas.js'
17
35
  import styles from './ArtifactForm.module.css'
18
36
 
37
+ /**
38
+ * Slugify a freeform string into the kebab-case form the `name` field expects.
39
+ * Mirrors the NAME_PATTERN in artifactSchemas.js — lowercase letters, digits,
40
+ * and hyphens. Used to auto-derive `name` from `title` while the user types,
41
+ * until they manually edit `name` themselves.
42
+ */
43
+ function slugifyName(value) {
44
+ return String(value || '')
45
+ .toLowerCase()
46
+ .normalize('NFKD')
47
+ .replace(/[\u0300-\u036f]/g, '')
48
+ .replace(/[^a-z0-9]+/g, '-')
49
+ .replace(/^-+|-+$/g, '')
50
+ .slice(0, 64)
51
+ }
52
+
19
53
  function TypeSelector({ selected, onChange }) {
20
54
  const types = Object.entries(ARTIFACT_SCHEMAS)
21
55
  const current = ARTIFACT_SCHEMAS[selected]
22
56
 
23
57
  return (
24
- <ActionMenu>
25
- <ActionMenu.Button className={styles.typeButton}>
26
- {current?.icon} {current?.label || 'Select type…'}
27
- </ActionMenu.Button>
28
- <ActionMenu.Overlay width="medium">
29
- <ActionList selectionVariant="single">
30
- {types.map(([key, schema]) => (
31
- <ActionList.Item key={key} selected={key === selected} onSelect={() => onChange(key)}>
32
- <ActionList.LeadingVisual>{schema.icon}</ActionList.LeadingVisual>
33
- {schema.label}
34
- <ActionList.Description variant="block">{schema.description}</ActionList.Description>
35
- </ActionList.Item>
36
- ))}
37
- </ActionList>
38
- </ActionMenu.Overlay>
39
- </ActionMenu>
58
+ <Menu>
59
+ <Menu.Button>
60
+ <span>{current?.icon}</span>
61
+ <span>{current?.label || 'Select type…'}</span>
62
+ </Menu.Button>
63
+ <Menu.Items>
64
+ {types.map(([key, schema]) => (
65
+ <Menu.Item
66
+ key={key}
67
+ selected={key === selected}
68
+ onSelect={() => onChange(key)}
69
+ description={schema.description}
70
+ >
71
+ <span>{schema.icon}</span>
72
+ <span>{schema.label}</span>
73
+ </Menu.Item>
74
+ ))}
75
+ </Menu.Items>
76
+ </Menu>
40
77
  )
41
78
  }
42
79
 
@@ -49,9 +86,8 @@ function FieldRenderer({ field, value, error, onChange, options }) {
49
86
  value={value || ''}
50
87
  onChange={e => onChange(e.target.value)}
51
88
  placeholder={field.placeholder}
52
- block
53
89
  type={field.type === 'url' ? 'url' : 'text'}
54
- validationStatus={error ? 'error' : undefined}
90
+ invalid={!!error}
55
91
  />
56
92
  )
57
93
  case 'textarea':
@@ -60,8 +96,8 @@ function FieldRenderer({ field, value, error, onChange, options }) {
60
96
  value={value || ''}
61
97
  onChange={e => onChange(e.target.value)}
62
98
  placeholder={field.placeholder}
63
- block
64
99
  rows={3}
100
+ invalid={!!error}
65
101
  />
66
102
  )
67
103
  case 'code':
@@ -70,52 +106,26 @@ function FieldRenderer({ field, value, error, onChange, options }) {
70
106
  value={value || ''}
71
107
  onChange={e => onChange(e.target.value)}
72
108
  placeholder={field.placeholder}
73
- block
74
109
  rows={8}
75
110
  className={styles.codeField}
111
+ invalid={!!error}
76
112
  />
77
113
  )
78
114
  case 'select':
79
- // options may be an array of strings or objects { value, label, group }.
80
- // When grouped, we render <optgroup>s so templates and recipes appear
81
- // under headings (mirrors the workshop form's grouping).
82
- {
83
- const norm = (options || []).map(o =>
84
- typeof o === 'string' ? { value: o, label: o, group: null } : { group: null, ...o }
85
- )
86
- const grouped = norm.some(o => o.group)
87
- return (
88
- <select
89
- className={styles.select}
90
- value={value || ''}
91
- onChange={e => onChange(e.target.value)}
92
- >
93
- <option value="">{field.placeholder || 'Select…'}</option>
94
- {grouped
95
- ? Object.entries(norm.reduce((acc, opt) => {
96
- const key = opt.group || 'Other'
97
- ;(acc[key] = acc[key] || []).push(opt)
98
- return acc
99
- }, {})).map(([group, opts]) => (
100
- <optgroup key={group} label={group}>
101
- {opts.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
102
- </optgroup>
103
- ))
104
- : norm.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)
105
- }
106
- </select>
107
- )
108
- }
115
+ return (
116
+ <Select
117
+ value={value || ''}
118
+ onChange={val => onChange(val)}
119
+ options={options || []}
120
+ placeholder={field.placeholder || 'Select…'}
121
+ invalid={!!error}
122
+ />
123
+ )
109
124
  case 'checkbox':
110
125
  return (
111
- <label className={styles.checkboxLabel}>
112
- <input
113
- type="checkbox"
114
- checked={!!value}
115
- onChange={e => onChange(e.target.checked)}
116
- />
117
- <span>{field.checkboxLabel || field.label}</span>
118
- </label>
126
+ <Checkbox checked={!!value} onChange={v => onChange(v)}>
127
+ {field.checkboxLabel || field.label}
128
+ </Checkbox>
119
129
  )
120
130
  default:
121
131
  return null
@@ -132,6 +142,38 @@ function initialValues(schema) {
132
142
  return initial
133
143
  }
134
144
 
145
+ /**
146
+ * Render one Field wrapper for a schema field. Checkbox fields hide the
147
+ * standalone label because the inline label inside the Checkbox already
148
+ * names the control — otherwise the label appears twice. Descriptions
149
+ * render under the input when there's no error (same pattern as
150
+ * `patternHint`).
151
+ */
152
+ function renderField(field, { values, errors, dynamicOptions, handleChange }) {
153
+ const options = field.dynamic ? dynamicOptions[field.dynamic] : field.options
154
+ const isCheckbox = field.type === 'checkbox'
155
+ const errorMessage = errors[field.name]
156
+ return (
157
+ <Field key={field.name} invalid={!!errorMessage}>
158
+ {isCheckbox
159
+ ? <Label visuallyHidden>{field.label}</Label>
160
+ : <Label required={field.required}>{field.label}</Label>
161
+ }
162
+ <FieldRenderer
163
+ field={field}
164
+ value={values[field.name]}
165
+ error={errorMessage}
166
+ onChange={val => handleChange(field.name, val)}
167
+ options={options}
168
+ />
169
+ {errorMessage && <Validation>{errorMessage}</Validation>}
170
+ {!errorMessage && (field.caption || field.patternHint) && (
171
+ <Description>{field.caption || field.patternHint}</Description>
172
+ )}
173
+ </Field>
174
+ )
175
+ }
176
+
135
177
  export default function ArtifactForm({
136
178
  type: fixedType,
137
179
  onSubmit,
@@ -150,18 +192,66 @@ export default function ArtifactForm({
150
192
  const [errors, setErrors] = useState({})
151
193
  const [submitting, setSubmitting] = useState(false)
152
194
 
195
+ // Tracks whether the `name` field should keep auto-following `title`.
196
+ // Flips to false the first time the user edits `name` directly, so we don't
197
+ // clobber their input. Reset on type change or unmount.
198
+ const nameAutoFollowRef = useRef(true)
199
+
153
200
  // Reset form when fixed type changes (parent-controlled)
154
201
  useEffect(() => {
155
202
  if (!fixedType) return
156
203
  setValues({ ...initialValues(ARTIFACT_SCHEMAS[fixedType]), ...(initialOverride || {}) })
157
204
  setErrors({})
158
205
  setSubmitting(false)
206
+ nameAutoFollowRef.current = true
159
207
  }, [fixedType])
160
208
 
209
+ // Merge late-arriving initial overrides (e.g. a gh-login fetch that resolves
210
+ // after mount) into empty fields. Only fills fields the user hasn't typed
211
+ // into yet, so we never overwrite their input. Stringify the override so the
212
+ // dep check fires on content changes rather than parent re-render identity.
213
+ const overrideKey = useMemo(() => JSON.stringify(initialOverride || {}), [initialOverride])
214
+ useEffect(() => {
215
+ if (!initialOverride || !schema) return
216
+ setValues(prev => {
217
+ let next = prev
218
+ for (const [k, v] of Object.entries(initialOverride)) {
219
+ if (v == null || v === '') continue
220
+ const fieldExists = schema.fields.some(f => f.name === k)
221
+ const isEmpty = prev[k] === '' || prev[k] == null
222
+ if (fieldExists && isEmpty) {
223
+ if (next === prev) next = { ...prev }
224
+ next[k] = v
225
+ }
226
+ }
227
+ return next
228
+ })
229
+ // eslint-disable-next-line react-hooks/exhaustive-deps
230
+ }, [overrideKey, schema])
231
+
232
+ // Derive `name` (kebab-case) from `title` as the user types, until they
233
+ // manually edit `name`. Only applies to schemas that expose both fields
234
+ // (prototype, canvas, flow). Component uses PascalCase and has no title,
235
+ // so it's naturally excluded.
236
+ useEffect(() => {
237
+ if (!schema || !nameAutoFollowRef.current) return
238
+ const hasName = schema.fields.some(f => f.name === 'name' && f.type === 'text')
239
+ const hasTitle = schema.fields.some(f => f.name === 'title')
240
+ if (!hasName || !hasTitle) return
241
+ const derived = slugifyName(values.title)
242
+ if (derived === (values.name || '')) return
243
+ setValues(prev => ({ ...prev, name: derived }))
244
+ if (errors.name) {
245
+ setErrors(prev => { const next = { ...prev }; delete next.name; return next })
246
+ }
247
+ // eslint-disable-next-line react-hooks/exhaustive-deps
248
+ }, [values.title, schema])
249
+
161
250
  function handleTypeChange(newType) {
162
251
  setSelectedType(newType)
163
252
  setValues(initialValues(ARTIFACT_SCHEMAS[newType]))
164
253
  setErrors({})
254
+ nameAutoFollowRef.current = true
165
255
  }
166
256
 
167
257
  const [showAdvanced, setShowAdvanced] = useState(false)
@@ -169,8 +259,11 @@ export default function ArtifactForm({
169
259
  const visibleFields = useMemo(() => {
170
260
  if (!schema) return []
171
261
  if (compact) return schema.fields.filter(f => f.required)
172
- // Hide mutually-exclusive partner once one side is set
173
262
  let fields = schema.fields
263
+ // Hide fields whose `showIf` predicate is not satisfied (e.g. external
264
+ // URL only appears when the "External prototype" checkbox is on).
265
+ fields = fields.filter(f => isFieldVisible(f, values))
266
+ // Hide mutually-exclusive partner once one side is set
174
267
  if (schema.mutuallyExclusive) {
175
268
  fields = fields.filter(f => {
176
269
  for (const group of schema.mutuallyExclusive) {
@@ -205,6 +298,9 @@ export default function ArtifactForm({
205
298
  }
206
299
 
207
300
  function handleChange(fieldName, value) {
301
+ if (fieldName === 'name') {
302
+ nameAutoFollowRef.current = false
303
+ }
208
304
  setValues(prev => ({ ...prev, [fieldName]: value }))
209
305
  if (errors[fieldName]) {
210
306
  setErrors(prev => { const next = { ...prev }; delete next[fieldName]; return next })
@@ -229,14 +325,19 @@ export default function ArtifactForm({
229
325
  }
230
326
 
231
327
  return (
232
- <form className={styles.form} onSubmit={handleSubmit} data-compact={compact || undefined} data-bare={hideHeader || undefined}>
328
+ <Form
329
+ className={styles.formShell}
330
+ onSubmit={handleSubmit}
331
+ data-compact={compact || undefined}
332
+ data-bare={hideHeader || undefined}
333
+ >
233
334
  {!hideHeader && (
234
335
  <header className={styles.header}>
235
336
  <h3 className={styles.title}>
236
337
  {operation === 'create' ? 'New' : 'Edit'} {schema.label}
237
338
  </h3>
238
339
  {!compact && (
239
- <Text as="p" className={styles.description}>{schema.description}</Text>
340
+ <p className={styles.description}>{schema.description}</p>
240
341
  )}
241
342
  </header>
242
343
  )}
@@ -248,29 +349,9 @@ export default function ArtifactForm({
248
349
  )}
249
350
 
250
351
  <div className={styles.fields}>
251
- {basicFields.map(field => {
252
- const options = field.dynamic ? dynamicOptions[field.dynamic] : field.options
253
- return (
254
- <FormControl key={field.name} required={field.required}>
255
- <FormControl.Label>{field.label}</FormControl.Label>
256
- <FieldRenderer
257
- field={field}
258
- value={values[field.name]}
259
- error={errors[field.name]}
260
- onChange={val => handleChange(field.name, val)}
261
- options={options}
262
- />
263
- {errors[field.name] && (
264
- <FormControl.Validation variant="error">
265
- {errors[field.name]}
266
- </FormControl.Validation>
267
- )}
268
- {field.patternHint && !errors[field.name] && (
269
- <FormControl.Caption>{field.patternHint}</FormControl.Caption>
270
- )}
271
- </FormControl>
272
- )
273
- })}
352
+ {basicFields.map(field => renderField(field, {
353
+ values, errors, dynamicOptions, handleChange,
354
+ }))}
274
355
 
275
356
  {advancedFields.length > 0 && (
276
357
  <>
@@ -283,29 +364,9 @@ export default function ArtifactForm({
283
364
  >
284
365
  {showAdvanced ? '− Hide advanced fields' : '+ Advanced fields'}
285
366
  </Button>
286
- {showAdvanced && advancedFields.map(field => {
287
- const options = field.dynamic ? dynamicOptions[field.dynamic] : field.options
288
- return (
289
- <FormControl key={field.name} required={field.required}>
290
- <FormControl.Label>{field.label}</FormControl.Label>
291
- <FieldRenderer
292
- field={field}
293
- value={values[field.name]}
294
- error={errors[field.name]}
295
- onChange={val => handleChange(field.name, val)}
296
- options={options}
297
- />
298
- {errors[field.name] && (
299
- <FormControl.Validation variant="error">
300
- {errors[field.name]}
301
- </FormControl.Validation>
302
- )}
303
- {field.patternHint && !errors[field.name] && (
304
- <FormControl.Caption>{field.patternHint}</FormControl.Caption>
305
- )}
306
- </FormControl>
307
- )
308
- })}
367
+ {showAdvanced && advancedFields.map(field => renderField(field, {
368
+ values, errors, dynamicOptions, handleChange,
369
+ }))}
309
370
  </>
310
371
  )}
311
372
  </div>
@@ -324,8 +385,8 @@ export default function ArtifactForm({
324
385
  {submitting ? 'Creating…' : `${operation === 'create' ? 'Create' : 'Save'} ${schema.label}`}
325
386
  </Button>
326
387
  </footer>
327
- </form>
388
+ </Form>
328
389
  )
329
390
  }
330
391
 
331
- export { TypeSelector, ARTIFACT_SCHEMAS, validateArtifact }
392
+ export { TypeSelector, ARTIFACT_SCHEMAS, validateArtifact, isFieldVisible }