@dfosco/storyboard-core 4.2.0-beta.2 → 4.2.0-beta.21

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 (414) hide show
  1. package/commandpalette.config.json +109 -24
  2. package/dist/storyboard-ui.css +1 -1
  3. package/dist/storyboard-ui.js +17379 -28568
  4. package/dist/storyboard-ui.js.map +1 -1
  5. package/dist/tailwind.css +1 -1
  6. package/package.json +5 -2
  7. package/scaffold/agents/prompt-agent.agent.md +181 -0
  8. package/scaffold/agents/terminal-agent.agent.md +351 -0
  9. package/scaffold/codex/config.toml +246 -0
  10. package/scaffold/manifest.json +5 -0
  11. package/scaffold/skills/canvas/SKILL.md +5 -4
  12. package/scaffold/skills/ship/SKILL.md +1 -1
  13. package/scaffold/storyboard.config.json +14 -1
  14. package/scaffold/toolbar.config.json +1 -1
  15. package/src/ActionMenuButton.jsx +100 -0
  16. package/src/AutosyncMenuButton.css +67 -0
  17. package/src/AutosyncMenuButton.jsx +241 -0
  18. package/src/BranchSelect.jsx +29 -0
  19. package/src/BranchSelect.module.css +30 -0
  20. package/src/CanvasAgentsMenu.jsx +87 -0
  21. package/src/CanvasCreateMenu.jsx +609 -0
  22. package/src/CanvasSnap.css +27 -0
  23. package/src/CanvasSnap.jsx +51 -0
  24. package/src/CanvasUndoRedo.css +36 -0
  25. package/src/CanvasUndoRedo.jsx +62 -0
  26. package/src/CanvasZoomControl.css +53 -0
  27. package/src/CanvasZoomControl.jsx +49 -0
  28. package/src/CanvasZoomToFit.css +18 -0
  29. package/src/CanvasZoomToFit.jsx +26 -0
  30. package/src/CommandMenu.css +8 -0
  31. package/src/CommandMenu.jsx +286 -0
  32. package/src/CommandPalette.jsx +35 -0
  33. package/src/CommandPaletteTrigger.jsx +25 -0
  34. package/src/CommentsMenuButton.jsx +38 -0
  35. package/src/CoreUIBar.css +47 -0
  36. package/src/CoreUIBar.jsx +855 -0
  37. package/src/CreateMenuButton.jsx +116 -0
  38. package/src/HideChromeTrigger.jsx +40 -0
  39. package/src/InspectorPanel.css +109 -0
  40. package/src/InspectorPanel.jsx +629 -0
  41. package/src/PwaInstallBanner.css +42 -0
  42. package/src/PwaInstallBanner.jsx +124 -0
  43. package/src/SidePanel.jsx +260 -0
  44. package/src/ThemeMenuButton.jsx +136 -0
  45. package/src/autosync/server.js +202 -5
  46. package/src/autosync/server.test.js +112 -0
  47. package/src/canvas/__tests__/agent-integration.test.js +593 -0
  48. package/src/canvas/__tests__/helpers/browser.js +95 -0
  49. package/src/canvas/__tests__/helpers/canvas-api.js +129 -0
  50. package/src/canvas/__tests__/helpers/perf.js +118 -0
  51. package/src/canvas/__tests__/helpers/setup.js +176 -0
  52. package/src/canvas/__tests__/helpers/tmux.js +130 -0
  53. package/src/canvas/__tests__/helpers/transcript.js +129 -0
  54. package/src/canvas/__tests__/terminal-integration.test.js +175 -0
  55. package/src/canvas/hot-pool.js +757 -0
  56. package/src/canvas/materializer.js +31 -0
  57. package/src/canvas/materializer.test.js +56 -0
  58. package/src/canvas/selectedWidgets.js +65 -7
  59. package/src/canvas/server.js +1801 -22
  60. package/src/canvas/server.test.js +239 -0
  61. package/src/canvas/terminal-config.js +331 -0
  62. package/src/canvas/terminal-registry.js +38 -0
  63. package/src/canvas/terminal-server.js +1037 -29
  64. package/src/canvas/writeGuard.js +51 -3
  65. package/src/canvasConfig.js +67 -1
  66. package/src/canvasConfig.test.js +79 -1
  67. package/src/cli/agent.js +85 -0
  68. package/src/cli/branch.js +232 -0
  69. package/src/cli/canvasAdd.js +59 -12
  70. package/src/cli/canvasBatch.js +98 -0
  71. package/src/cli/canvasBounds.js +1 -1
  72. package/src/cli/canvasRead.js +1 -1
  73. package/src/cli/canvasUpdate.js +179 -0
  74. package/src/cli/create.js +38 -14
  75. package/src/cli/dev.js +157 -83
  76. package/src/cli/exit.js +23 -24
  77. package/src/cli/index.js +55 -2
  78. package/src/cli/proxy.js +96 -37
  79. package/src/cli/schemas.js +22 -4
  80. package/src/cli/server.js +148 -25
  81. package/src/cli/serverUrl.js +8 -3
  82. package/src/cli/sessions.js +131 -5
  83. package/src/cli/setup.js +109 -11
  84. package/src/cli/terminal-commands.js +16 -8
  85. package/src/cli/terminal-messaging.js +231 -0
  86. package/src/cli/terminal-welcome.js +365 -33
  87. package/src/commandActions.js +1 -0
  88. package/src/commandPaletteConfig.js +9 -0
  89. package/src/comments/auth.js +2 -1
  90. package/src/comments/ui/AuthModal.jsx +114 -0
  91. package/src/comments/ui/CommentWindow.jsx +329 -0
  92. package/src/comments/ui/CommentsDrawer.jsx +102 -0
  93. package/src/comments/ui/Composer.jsx +64 -0
  94. package/src/comments/ui/authModal.test.js +1 -1
  95. package/src/comments/ui/commentWindow.js +16 -17
  96. package/src/comments/ui/commentsDrawer.js +25 -26
  97. package/src/comments/ui/composer.js +23 -24
  98. package/src/comments/ui/index.js +2 -3
  99. package/src/configSchema.js +59 -1
  100. package/src/configStore.js +161 -0
  101. package/src/core-ui-colors.css +12 -0
  102. package/src/devtools.js +17 -19
  103. package/src/devtools.test.js +18 -9
  104. package/src/featureFlags.js +12 -5
  105. package/src/fuzzySearch.test.js +10 -0
  106. package/src/index.js +14 -2
  107. package/src/lib/components/ui/alert/alert-action.jsx +11 -0
  108. package/src/lib/components/ui/alert/alert-description.jsx +11 -0
  109. package/src/lib/components/ui/alert/alert-title.jsx +11 -0
  110. package/src/lib/components/ui/alert/alert.jsx +25 -0
  111. package/src/lib/components/ui/alert/index.js +15 -15
  112. package/src/lib/components/ui/avatar/avatar-badge.jsx +22 -0
  113. package/src/lib/components/ui/avatar/avatar-fallback.jsx +18 -0
  114. package/src/lib/components/ui/avatar/avatar-group-count.jsx +19 -0
  115. package/src/lib/components/ui/avatar/avatar-group.jsx +19 -0
  116. package/src/lib/components/ui/avatar/avatar-image.jsx +15 -0
  117. package/src/lib/components/ui/avatar/avatar.jsx +19 -0
  118. package/src/lib/components/ui/avatar/index.js +20 -20
  119. package/src/lib/components/ui/badge/badge.jsx +31 -0
  120. package/src/lib/components/ui/badge/index.js +2 -2
  121. package/src/lib/components/ui/button/button.jsx +100 -0
  122. package/src/lib/components/ui/button/index.js +9 -9
  123. package/src/lib/components/ui/card/card-action.jsx +11 -0
  124. package/src/lib/components/ui/card/card-content.jsx +11 -0
  125. package/src/lib/components/ui/card/card-description.jsx +11 -0
  126. package/src/lib/components/ui/card/card-footer.jsx +11 -0
  127. package/src/lib/components/ui/card/card-header.jsx +19 -0
  128. package/src/lib/components/ui/card/card-title.jsx +11 -0
  129. package/src/lib/components/ui/card/card.jsx +17 -0
  130. package/src/lib/components/ui/card/index.js +23 -23
  131. package/src/lib/components/ui/checkbox/checkbox.jsx +29 -0
  132. package/src/lib/components/ui/checkbox/index.js +5 -5
  133. package/src/lib/components/ui/collapsible/collapsible-content.jsx +7 -0
  134. package/src/lib/components/ui/collapsible/collapsible-trigger.jsx +7 -0
  135. package/src/lib/components/ui/collapsible/collapsible.jsx +7 -0
  136. package/src/lib/components/ui/collapsible/index.js +11 -11
  137. package/src/lib/components/ui/dialog/dialog-close.jsx +7 -0
  138. package/src/lib/components/ui/dialog/dialog-content.jsx +34 -0
  139. package/src/lib/components/ui/dialog/dialog-description.jsx +15 -0
  140. package/src/lib/components/ui/dialog/dialog-footer.jsx +23 -0
  141. package/src/lib/components/ui/dialog/dialog-header.jsx +11 -0
  142. package/src/lib/components/ui/dialog/dialog-overlay.jsx +15 -0
  143. package/src/lib/components/ui/dialog/dialog-portal.jsx +4 -0
  144. package/src/lib/components/ui/dialog/dialog-title.jsx +15 -0
  145. package/src/lib/components/ui/dialog/dialog-trigger.jsx +7 -0
  146. package/src/lib/components/ui/dialog/dialog.jsx +4 -0
  147. package/src/lib/components/ui/dialog/index.js +32 -32
  148. package/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +8 -0
  149. package/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +30 -0
  150. package/src/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +22 -0
  151. package/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +16 -0
  152. package/src/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +7 -0
  153. package/src/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +20 -0
  154. package/src/lib/components/ui/dropdown-menu/dropdown-menu-label.jsx +17 -0
  155. package/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +4 -0
  156. package/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +7 -0
  157. package/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +29 -0
  158. package/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +15 -0
  159. package/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.jsx +16 -0
  160. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +15 -0
  161. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +23 -0
  162. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +4 -0
  163. package/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +7 -0
  164. package/src/lib/components/ui/dropdown-menu/dropdown-menu.jsx +4 -0
  165. package/src/lib/components/ui/dropdown-menu/index.js +52 -52
  166. package/src/lib/components/ui/input/index.js +5 -5
  167. package/src/lib/components/ui/input/input.jsx +19 -0
  168. package/src/lib/components/ui/label/index.js +5 -5
  169. package/src/lib/components/ui/label/label.jsx +19 -0
  170. package/src/lib/components/ui/panel/index.js +21 -21
  171. package/src/lib/components/ui/panel/panel-body.jsx +11 -0
  172. package/src/lib/components/ui/panel/panel-close.jsx +16 -0
  173. package/src/lib/components/ui/panel/panel-content.jsx +29 -0
  174. package/src/lib/components/ui/panel/panel-footer.jsx +11 -0
  175. package/src/lib/components/ui/panel/panel-header.jsx +11 -0
  176. package/src/lib/components/ui/panel/panel-title.jsx +12 -0
  177. package/src/lib/components/ui/panel/panel.jsx +4 -0
  178. package/src/lib/components/ui/popover/index.js +26 -26
  179. package/src/lib/components/ui/popover/popover-close.jsx +7 -0
  180. package/src/lib/components/ui/popover/popover-content.jsx +22 -0
  181. package/src/lib/components/ui/popover/popover-description.jsx +11 -0
  182. package/src/lib/components/ui/popover/popover-header.jsx +11 -0
  183. package/src/lib/components/ui/popover/popover-portal.jsx +4 -0
  184. package/src/lib/components/ui/popover/popover-title.jsx +11 -0
  185. package/src/lib/components/ui/popover/popover-trigger.jsx +8 -0
  186. package/src/lib/components/ui/popover/popover.jsx +4 -0
  187. package/src/lib/components/ui/searchable-list.jsx +159 -0
  188. package/src/lib/components/ui/select/index.js +35 -35
  189. package/src/lib/components/ui/select/select-content.jsx +30 -0
  190. package/src/lib/components/ui/select/select-group-heading.jsx +17 -0
  191. package/src/lib/components/ui/select/select-group.jsx +15 -0
  192. package/src/lib/components/ui/select/select-item.jsx +26 -0
  193. package/src/lib/components/ui/select/select-label.jsx +11 -0
  194. package/src/lib/components/ui/select/select-portal.jsx +4 -0
  195. package/src/lib/components/ui/select/select-scroll-down-button.jsx +18 -0
  196. package/src/lib/components/ui/select/select-scroll-up-button.jsx +18 -0
  197. package/src/lib/components/ui/select/select-separator.jsx +15 -0
  198. package/src/lib/components/ui/select/select-trigger.jsx +25 -0
  199. package/src/lib/components/ui/select/select.jsx +4 -0
  200. package/src/lib/components/ui/separator/index.js +5 -5
  201. package/src/lib/components/ui/separator/separator.jsx +22 -0
  202. package/src/lib/components/ui/sheet/index.js +32 -32
  203. package/src/lib/components/ui/sheet/sheet-close.jsx +7 -0
  204. package/src/lib/components/ui/sheet/sheet-content.jsx +35 -0
  205. package/src/lib/components/ui/sheet/sheet-description.jsx +15 -0
  206. package/src/lib/components/ui/sheet/sheet-footer.jsx +11 -0
  207. package/src/lib/components/ui/sheet/sheet-header.jsx +11 -0
  208. package/src/lib/components/ui/sheet/sheet-overlay.jsx +15 -0
  209. package/src/lib/components/ui/sheet/sheet-portal.jsx +4 -0
  210. package/src/lib/components/ui/sheet/sheet-title.jsx +15 -0
  211. package/src/lib/components/ui/sheet/sheet-trigger.jsx +7 -0
  212. package/src/lib/components/ui/sheet/sheet.jsx +4 -0
  213. package/src/lib/components/ui/textarea/index.js +5 -5
  214. package/src/lib/components/ui/textarea/textarea.jsx +18 -0
  215. package/src/lib/components/ui/toggle/index.js +6 -9
  216. package/src/lib/components/ui/toggle/toggle.jsx +36 -0
  217. package/src/lib/components/ui/toggle-group/index.js +8 -8
  218. package/src/lib/components/ui/toggle-group/toggle-group-item.jsx +29 -0
  219. package/src/lib/components/ui/toggle-group/toggle-group.jsx +43 -0
  220. package/src/lib/components/ui/tooltip/index.js +3 -3
  221. package/src/lib/components/ui/tooltip/tooltip-content.jsx +21 -0
  222. package/src/lib/components/ui/tooltip/tooltip-trigger.jsx +23 -0
  223. package/src/lib/components/ui/tooltip/tooltip.jsx +11 -0
  224. package/src/lib/components/ui/trigger-button/index.js +3 -3
  225. package/src/lib/components/ui/trigger-button/trigger-button.css +38 -0
  226. package/src/lib/components/ui/trigger-button/trigger-button.jsx +63 -0
  227. package/src/logger/devLogger.js +238 -0
  228. package/src/logger/devLogger.test.js +193 -0
  229. package/src/modes.test.js +4 -4
  230. package/src/mountStoryboardCore.js +123 -27
  231. package/src/paletteProviders.js +3 -0
  232. package/src/paletteProviders.test.js +2 -2
  233. package/src/server/index.js +98 -36
  234. package/src/sidepanel.css +214 -0
  235. package/src/styles/tailwind.css +1 -1
  236. package/src/svelte-plugin-ui/__tests__/ModeSwitch.test.ts +8 -8
  237. package/src/svelte-plugin-ui/__tests__/ToolbarShell.test.ts +11 -10
  238. package/src/svelte-plugin-ui/components/Icon.css +11 -0
  239. package/src/svelte-plugin-ui/components/Icon.jsx +281 -0
  240. package/src/svelte-plugin-ui/components/ModeSwitch.css +90 -0
  241. package/src/svelte-plugin-ui/components/ModeSwitch.jsx +47 -0
  242. package/src/svelte-plugin-ui/components/ToolbarShell.css +80 -0
  243. package/src/svelte-plugin-ui/components/ToolbarShell.jsx +84 -0
  244. package/src/svelte-plugin-ui/components/Viewfinder.css +412 -0
  245. package/src/svelte-plugin-ui/components/Viewfinder.jsx +512 -0
  246. package/src/svelte-plugin-ui/mount.ts +12 -16
  247. package/src/toolRegistry.js +4 -4
  248. package/src/toolbarConfigStore.js +30 -0
  249. package/src/tools/handlers/autosync.js +1 -1
  250. package/src/tools/handlers/canvasAddWidget.js +1 -1
  251. package/src/tools/handlers/canvasAgents.js +19 -0
  252. package/src/tools/handlers/canvasToolbar.js +8 -8
  253. package/src/tools/handlers/commandPalette.js +9 -0
  254. package/src/tools/handlers/comments.js +1 -1
  255. package/src/tools/handlers/create.js +1 -1
  256. package/src/tools/handlers/devtools.js +16 -0
  257. package/src/tools/handlers/devtools.test.js +38 -0
  258. package/src/tools/handlers/flows.js +1 -1
  259. package/src/tools/handlers/hideChrome.js +9 -0
  260. package/src/tools/handlers/paletteTheme.js +35 -0
  261. package/src/tools/handlers/theme.js +1 -1
  262. package/src/tools/registry.js +4 -1
  263. package/src/tools/surfaces/commandList.js +3 -3
  264. package/src/tools/surfaces/mainToolbar.js +3 -3
  265. package/src/tools/surfaces/registry.js +4 -4
  266. package/src/ui/design-modes.ts +2 -2
  267. package/src/ui/viewfinder.ts +1 -1
  268. package/src/vite/server-plugin.js +242 -60
  269. package/src/workshop/features/createCanvas/CreateCanvasForm.jsx +260 -0
  270. package/src/workshop/features/createCanvas/index.js +1 -1
  271. package/src/workshop/features/createFlow/CreateFlowForm.jsx +334 -0
  272. package/src/workshop/features/createFlow/index.js +1 -1
  273. package/src/workshop/features/createPage/CreatePageForm.jsx +304 -0
  274. package/src/workshop/features/createPage/index.js +1 -1
  275. package/src/workshop/features/createPrototype/CreatePrototypeForm.jsx +289 -0
  276. package/src/workshop/features/createPrototype/index.js +1 -1
  277. package/src/workshop/features/createPrototype/server.js +98 -0
  278. package/src/workshop/features/createStory/CreateStoryForm.jsx +208 -0
  279. package/src/workshop/features/createStory/index.js +1 -1
  280. package/src/workshop/ui/WorkshopPanel.jsx +98 -0
  281. package/src/workshop/ui/mount.ts +1 -1
  282. package/src/worktree/port.js +48 -0
  283. package/src/worktree/serverRegistry.js +120 -0
  284. package/toolbar.config.json +93 -42
  285. package/widgets.config.json +580 -12
  286. package/src/ActionMenuButton.svelte +0 -119
  287. package/src/AutosyncMenuButton.svelte +0 -397
  288. package/src/CanvasCreateMenu.svelte +0 -295
  289. package/src/CanvasSnap.svelte +0 -87
  290. package/src/CanvasUndoRedo.svelte +0 -108
  291. package/src/CanvasZoomControl.svelte +0 -111
  292. package/src/CanvasZoomToFit.svelte +0 -52
  293. package/src/CommandMenu.svelte +0 -249
  294. package/src/CommandPalette.svelte +0 -33
  295. package/src/CommentsMenuButton.svelte +0 -53
  296. package/src/CoreUIBar.svelte +0 -847
  297. package/src/CreateMenuButton.svelte +0 -133
  298. package/src/DocPanel.svelte +0 -299
  299. package/src/InspectorPanel.svelte +0 -745
  300. package/src/PwaInstallBanner.svelte +0 -124
  301. package/src/SidePanel.svelte +0 -480
  302. package/src/ThemeMenuButton.svelte +0 -132
  303. package/src/comments/ui/AuthModal.svelte +0 -108
  304. package/src/comments/ui/CommentWindow.svelte +0 -333
  305. package/src/comments/ui/CommentsDrawer.svelte +0 -96
  306. package/src/comments/ui/Composer.svelte +0 -65
  307. package/src/lib/components/ui/alert/alert-action.svelte +0 -19
  308. package/src/lib/components/ui/alert/alert-description.svelte +0 -22
  309. package/src/lib/components/ui/alert/alert-title.svelte +0 -22
  310. package/src/lib/components/ui/alert/alert.svelte +0 -38
  311. package/src/lib/components/ui/avatar/avatar-badge.svelte +0 -25
  312. package/src/lib/components/ui/avatar/avatar-fallback.svelte +0 -20
  313. package/src/lib/components/ui/avatar/avatar-group-count.svelte +0 -22
  314. package/src/lib/components/ui/avatar/avatar-group.svelte +0 -22
  315. package/src/lib/components/ui/avatar/avatar-image.svelte +0 -17
  316. package/src/lib/components/ui/avatar/avatar.svelte +0 -24
  317. package/src/lib/components/ui/badge/badge.svelte +0 -44
  318. package/src/lib/components/ui/button/button.svelte +0 -108
  319. package/src/lib/components/ui/card/card-action.svelte +0 -21
  320. package/src/lib/components/ui/card/card-content.svelte +0 -19
  321. package/src/lib/components/ui/card/card-description.svelte +0 -19
  322. package/src/lib/components/ui/card/card-footer.svelte +0 -18
  323. package/src/lib/components/ui/card/card-header.svelte +0 -21
  324. package/src/lib/components/ui/card/card-title.svelte +0 -14
  325. package/src/lib/components/ui/card/card.svelte +0 -21
  326. package/src/lib/components/ui/checkbox/checkbox.svelte +0 -39
  327. package/src/lib/components/ui/collapsible/collapsible-content.svelte +0 -7
  328. package/src/lib/components/ui/collapsible/collapsible-trigger.svelte +0 -7
  329. package/src/lib/components/ui/collapsible/collapsible.svelte +0 -11
  330. package/src/lib/components/ui/dialog/dialog-close.svelte +0 -11
  331. package/src/lib/components/ui/dialog/dialog-content.svelte +0 -42
  332. package/src/lib/components/ui/dialog/dialog-description.svelte +0 -17
  333. package/src/lib/components/ui/dialog/dialog-footer.svelte +0 -29
  334. package/src/lib/components/ui/dialog/dialog-header.svelte +0 -19
  335. package/src/lib/components/ui/dialog/dialog-overlay.svelte +0 -17
  336. package/src/lib/components/ui/dialog/dialog-portal.svelte +0 -7
  337. package/src/lib/components/ui/dialog/dialog-title.svelte +0 -17
  338. package/src/lib/components/ui/dialog/dialog-trigger.svelte +0 -11
  339. package/src/lib/components/ui/dialog/dialog.svelte +0 -7
  340. package/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +0 -16
  341. package/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +0 -40
  342. package/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +0 -27
  343. package/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +0 -18
  344. package/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte +0 -7
  345. package/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +0 -24
  346. package/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +0 -20
  347. package/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte +0 -7
  348. package/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +0 -16
  349. package/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +0 -34
  350. package/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte +0 -17
  351. package/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +0 -19
  352. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +0 -17
  353. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +0 -27
  354. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte +0 -7
  355. package/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +0 -7
  356. package/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte +0 -7
  357. package/src/lib/components/ui/input/input.svelte +0 -40
  358. package/src/lib/components/ui/label/label.svelte +0 -20
  359. package/src/lib/components/ui/panel/panel-body.svelte +0 -13
  360. package/src/lib/components/ui/panel/panel-close.svelte +0 -16
  361. package/src/lib/components/ui/panel/panel-content.svelte +0 -33
  362. package/src/lib/components/ui/panel/panel-footer.svelte +0 -13
  363. package/src/lib/components/ui/panel/panel-header.svelte +0 -16
  364. package/src/lib/components/ui/panel/panel-title.svelte +0 -14
  365. package/src/lib/components/ui/panel/panel.svelte +0 -15
  366. package/src/lib/components/ui/popover/popover-close.svelte +0 -7
  367. package/src/lib/components/ui/popover/popover-content.svelte +0 -27
  368. package/src/lib/components/ui/popover/popover-description.svelte +0 -19
  369. package/src/lib/components/ui/popover/popover-header.svelte +0 -19
  370. package/src/lib/components/ui/popover/popover-portal.svelte +0 -7
  371. package/src/lib/components/ui/popover/popover-title.svelte +0 -19
  372. package/src/lib/components/ui/popover/popover-trigger.svelte +0 -17
  373. package/src/lib/components/ui/popover/popover.svelte +0 -7
  374. package/src/lib/components/ui/select/select-content.svelte +0 -40
  375. package/src/lib/components/ui/select/select-group-heading.svelte +0 -19
  376. package/src/lib/components/ui/select/select-group.svelte +0 -17
  377. package/src/lib/components/ui/select/select-item.svelte +0 -38
  378. package/src/lib/components/ui/select/select-label.svelte +0 -18
  379. package/src/lib/components/ui/select/select-portal.svelte +0 -7
  380. package/src/lib/components/ui/select/select-scroll-down-button.svelte +0 -20
  381. package/src/lib/components/ui/select/select-scroll-up-button.svelte +0 -20
  382. package/src/lib/components/ui/select/select-separator.svelte +0 -17
  383. package/src/lib/components/ui/select/select-trigger.svelte +0 -27
  384. package/src/lib/components/ui/select/select.svelte +0 -11
  385. package/src/lib/components/ui/separator/separator.svelte +0 -23
  386. package/src/lib/components/ui/sheet/sheet-close.svelte +0 -7
  387. package/src/lib/components/ui/sheet/sheet-content.svelte +0 -43
  388. package/src/lib/components/ui/sheet/sheet-description.svelte +0 -17
  389. package/src/lib/components/ui/sheet/sheet-footer.svelte +0 -18
  390. package/src/lib/components/ui/sheet/sheet-header.svelte +0 -19
  391. package/src/lib/components/ui/sheet/sheet-overlay.svelte +0 -17
  392. package/src/lib/components/ui/sheet/sheet-portal.svelte +0 -7
  393. package/src/lib/components/ui/sheet/sheet-title.svelte +0 -17
  394. package/src/lib/components/ui/sheet/sheet-trigger.svelte +0 -7
  395. package/src/lib/components/ui/sheet/sheet.svelte +0 -7
  396. package/src/lib/components/ui/textarea/textarea.svelte +0 -21
  397. package/src/lib/components/ui/toggle/toggle.svelte +0 -45
  398. package/src/lib/components/ui/toggle-group/toggle-group-item.svelte +0 -35
  399. package/src/lib/components/ui/toggle-group/toggle-group.svelte +0 -63
  400. package/src/lib/components/ui/tooltip/tooltip-content.svelte +0 -24
  401. package/src/lib/components/ui/tooltip/tooltip-trigger.svelte +0 -27
  402. package/src/lib/components/ui/tooltip/tooltip.svelte +0 -9
  403. package/src/lib/components/ui/trigger-button/trigger-button.svelte +0 -106
  404. package/src/svelte-plugin-ui/components/Icon.svelte +0 -181
  405. package/src/svelte-plugin-ui/components/ModeSwitch.svelte +0 -121
  406. package/src/svelte-plugin-ui/components/ToolbarShell.svelte +0 -150
  407. package/src/svelte-plugin-ui/components/Viewfinder.svelte +0 -1001
  408. package/src/tools/handlers/docs.js +0 -11
  409. package/src/workshop/features/createCanvas/CreateCanvasForm.svelte +0 -139
  410. package/src/workshop/features/createFlow/CreateFlowForm.svelte +0 -314
  411. package/src/workshop/features/createPage/CreatePageForm.svelte +0 -249
  412. package/src/workshop/features/createPrototype/CreatePrototypeForm.svelte +0 -287
  413. package/src/workshop/features/createStory/CreateStoryForm.svelte +0 -161
  414. package/src/workshop/ui/WorkshopPanel.svelte +0 -97
@@ -0,0 +1,329 @@
1
+ /**
2
+ * CommentWindow — thread viewer popup showing a comment with replies and reactions.
3
+ * Uses shadcn Button, Textarea, Avatar, Badge, Separator.
4
+ */
5
+
6
+ import { useState, useMemo } from 'react'
7
+ import { replyToComment, addReaction, removeReaction, resolveComment, unresolveComment, editComment, editReply, deleteComment } from '../api.js'
8
+ import { saveDraft, getDraft, clearDraft, replyDraftKey } from '../commentDrafts.js'
9
+ import { Button } from '../../lib/components/ui/button/index.js'
10
+ import { Textarea } from '../../lib/components/ui/textarea/index.js'
11
+ import * as Avatar from '../../lib/components/ui/avatar/index.js'
12
+ import { Separator } from '../../lib/components/ui/separator/index.js'
13
+ import { cn } from '../../lib/utils/index.js'
14
+
15
+ const REACTION_EMOJI = {
16
+ THUMBS_UP: '👍', THUMBS_DOWN: '👎', LAUGH: '😄', HOORAY: '🎉',
17
+ CONFUSED: '😕', HEART: '❤️', ROCKET: '🚀', EYES: '👀',
18
+ }
19
+ const emojiEntries = Object.entries(REACTION_EMOJI)
20
+
21
+ function timeAgo(dateStr) {
22
+ return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
23
+ }
24
+
25
+ function emojiFor(content) { return REACTION_EMOJI[content] ?? content }
26
+
27
+ function pillClass(active) {
28
+ return cn(
29
+ 'inline-flex items-center text-xs px-2 py-0.5 rounded-full border cursor-pointer transition-colors',
30
+ active ? 'border-primary bg-primary/10 text-primary' : 'border-border bg-transparent text-muted-foreground'
31
+ )
32
+ }
33
+
34
+ function emojiPickerBtnClass(active) {
35
+ return cn(
36
+ 'flex items-center justify-center w-7 h-7 rounded border-none text-base cursor-pointer transition-colors',
37
+ active ? 'bg-primary/10' : 'bg-transparent hover:bg-muted'
38
+ )
39
+ }
40
+
41
+ export default function CommentWindow({ comment, discussion, user = null, onClose, onMove, winEl }) {
42
+ const draftKey = useMemo(() => replyDraftKey(comment.id), [comment.id])
43
+
44
+ const [resolving, setResolving] = useState(false)
45
+ const [copied, setCopied] = useState(false)
46
+ const [editing, setEditing] = useState(false)
47
+ const [editText, setEditText] = useState('')
48
+ const [saving, setSaving] = useState(false)
49
+ const [replyText, setReplyText] = useState(() => getDraft(draftKey)?.text ?? '')
50
+ const [submittingReply, setSubmittingReply] = useState(false)
51
+ const [editingReply, setEditingReply] = useState(-1)
52
+ const [editReplyText, setEditReplyText] = useState('')
53
+ const [savingReply, setSavingReply] = useState(false)
54
+ const [pickerTarget, setPickerTarget] = useState(null)
55
+ const [reactions, setReactions] = useState(() => [...(comment.reactionGroups ?? [])])
56
+ const [replyReactions, setReplyReactions] = useState(() => (comment.replies ?? []).map((r) => [...(r.reactionGroups ?? [])]))
57
+ const [replyTexts, setReplyTexts] = useState(() => (comment.replies ?? []).map((r) => r.text ?? r.body ?? ''))
58
+
59
+ const resolved = !!comment.meta?.resolved
60
+ const commentText = comment.text ?? ''
61
+ const replies = comment.replies ?? []
62
+ const canEdit = !!(user && comment.author?.login === user.login)
63
+ const canReply = !!(user && discussion)
64
+
65
+ function handleReplyBlur() {
66
+ if (replyText.trim()) {
67
+ saveDraft(draftKey, { type: 'reply', text: replyText })
68
+ } else {
69
+ clearDraft(draftKey)
70
+ }
71
+ }
72
+
73
+ function isReacted(content) { return reactions.some((r) => r.content === content && r.viewerHasReacted) }
74
+ function isReplyReacted(ri, content) { return (replyReactions[ri] ?? []).some((r) => r.content === content && r.viewerHasReacted) }
75
+
76
+ async function toggleResolve() {
77
+ setResolving(true)
78
+ try {
79
+ if (resolved) {
80
+ await unresolveComment(comment.id, comment._rawBody ?? comment.body ?? '')
81
+ comment.meta = { ...comment.meta }; delete comment.meta.resolved
82
+ setResolving(false); onMove?.()
83
+ } else {
84
+ await resolveComment(comment.id, comment._rawBody ?? comment.body ?? '')
85
+ comment.meta = { ...comment.meta, resolved: true }; onMove?.(); onClose?.()
86
+ }
87
+ } catch (err) { console.error('[storyboard] Failed to toggle resolve:', err); setResolving(false) }
88
+ }
89
+
90
+ function copyLink() {
91
+ const url = new URL(window.location.href); url.searchParams.set('comment', comment.id)
92
+ navigator.clipboard.writeText(url.toString()).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000) }).catch(() => {})
93
+ }
94
+
95
+ async function saveEdit() {
96
+ const t = editText.trim(); if (!t) return; setSaving(true)
97
+ try { await editComment(comment.id, comment._rawBody ?? comment.body ?? '', t); comment.text = t; comment._rawBody = null; setEditing(false) } catch (err) { console.error('[storyboard] Failed to edit:', err) } finally { setSaving(false) }
98
+ }
99
+
100
+ async function toggleReaction(content, gi) {
101
+ const group = gi !== undefined ? reactions[gi] : reactions.find((r) => r.content === content)
102
+ const was = group?.viewerHasReacted ?? false
103
+ let next
104
+ if (was && group) {
105
+ group.users = { totalCount: Math.max(0, (group.users?.totalCount ?? 1) - 1) }; group.viewerHasReacted = false
106
+ next = group.users.totalCount === 0 ? reactions.filter((r) => r.content !== content) : [...reactions]
107
+ } else if (group) {
108
+ group.users = { totalCount: (group.users?.totalCount ?? 0) + 1 }; group.viewerHasReacted = true; next = [...reactions]
109
+ } else {
110
+ next = [...reactions, { content, users: { totalCount: 1 }, viewerHasReacted: true }]
111
+ }
112
+ setReactions(next)
113
+ comment.reactionGroups = next
114
+ try { if (was) await removeReaction(comment.id, content); else await addReaction(comment.id, content) } catch { /* ignore */ }
115
+ }
116
+
117
+ async function toggleReplyReaction(ri, content, rgi) {
118
+ const reply = replies[ri]; if (!reply) return
119
+ const groups = [...(replyReactions[ri] ?? [])]
120
+ const group = rgi !== undefined ? groups[rgi] : groups.find((r) => r.content === content)
121
+ const was = group?.viewerHasReacted ?? false
122
+ if (was && group) {
123
+ group.users = { totalCount: Math.max(0, (group.users?.totalCount ?? 1) - 1) }; group.viewerHasReacted = false
124
+ if (group.users.totalCount === 0) groups.splice(groups.indexOf(group), 1)
125
+ } else if (group) {
126
+ group.users = { totalCount: (group.users?.totalCount ?? 0) + 1 }; group.viewerHasReacted = true
127
+ } else {
128
+ groups.push({ content, users: { totalCount: 1 }, viewerHasReacted: true })
129
+ }
130
+ const nextRR = [...replyReactions]; nextRR[ri] = groups
131
+ setReplyReactions(nextRR)
132
+ reply.reactionGroups = groups
133
+ try { if (was) await removeReaction(reply.id, content); else await addReaction(reply.id, content) } catch { /* ignore */ }
134
+ }
135
+
136
+ async function submitReply() {
137
+ const t = replyText.trim(); if (!t) return; setSubmittingReply(true)
138
+ try {
139
+ await replyToComment(discussion.id, comment.id, t)
140
+ setReplyText('')
141
+ clearDraft(draftKey)
142
+ onMove?.()
143
+ } catch (err) { console.error('[storyboard] Reply failed:', err) } finally { setSubmittingReply(false) }
144
+ }
145
+
146
+ async function saveReply(ri) {
147
+ const t = editReplyText.trim(); if (!t) return
148
+ const reply = replies[ri]; if (!reply) return; setSavingReply(true)
149
+ try {
150
+ await editReply(reply.id, t); reply.text = t; reply.body = t
151
+ const nextTexts = [...replyTexts]; nextTexts[ri] = t; setReplyTexts(nextTexts)
152
+ setEditingReply(-1)
153
+ } catch (err) { console.error('[storyboard] Edit reply failed:', err) } finally { setSavingReply(false) }
154
+ }
155
+
156
+ async function deleteReplyAt(ri) {
157
+ const reply = replies[ri]; if (!reply || !confirm('Delete this reply?')) return
158
+ try { await deleteComment(reply.id); onMove?.() } catch (err) { console.error('[storyboard] Delete failed:', err) }
159
+ }
160
+
161
+ function startDrag(e) {
162
+ const target = e.target
163
+ if (target.closest('[data-no-drag]') || !winEl) return
164
+ const startX = e.clientX, startY = e.clientY, rect = winEl.getBoundingClientRect()
165
+ const sx = rect.left, sy = rect.top; target.style.cursor = 'grabbing'
166
+ const mv = (ev) => { winEl.style.position = 'fixed'; winEl.style.left = `${sx + ev.clientX - startX}px`; winEl.style.top = `${sy + ev.clientY - startY}px`; winEl.style.transform = 'none' }
167
+ const up = () => { target.style.cursor = 'grab'; document.removeEventListener('mousemove', mv); document.removeEventListener('mouseup', up) }
168
+ document.addEventListener('mousemove', mv); document.addEventListener('mouseup', up); e.preventDefault()
169
+ }
170
+
171
+ function handleReplyKeydown(e) {
172
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); submitReply() }
173
+ }
174
+
175
+ return (
176
+ <div className="font-sans" onClick={(e) => e.stopPropagation()}>
177
+ {/* Header */}
178
+ <div className="flex items-center justify-between px-3 py-3 border-b border-border cursor-grab select-none" onMouseDown={startDrag}>
179
+ <div className="flex items-center gap-2">
180
+ {comment.author?.avatarUrl && (
181
+ <Avatar.Root className="h-6 w-6">
182
+ <Avatar.Image src={comment.author.avatarUrl} alt={comment.author?.login} />
183
+ <Avatar.Fallback className="text-[10px]">{(comment.author?.login ?? '?')[0]?.toUpperCase()}</Avatar.Fallback>
184
+ </Avatar.Root>
185
+ )}
186
+ <div className="flex flex-col">
187
+ <span className="text-xs font-semibold">{comment.author?.login ?? 'unknown'}</span>
188
+ {comment.createdAt && (
189
+ <span className="text-[11px] text-muted-foreground leading-tight">{timeAgo(comment.createdAt)}</span>
190
+ )}
191
+ </div>
192
+ </div>
193
+ <div className="flex items-center shrink-0 gap-1.5" data-no-drag onMouseDown={(e) => e.stopPropagation()}>
194
+ <Button variant="ghost" size="sm" className={cn('h-6 px-2 text-[11px]', resolved ? 'text-success' : 'text-muted-foreground')} disabled={resolving} onClick={toggleResolve}>
195
+ {resolving ? (resolved ? 'Unresolving…' : 'Resolving…') : (resolved ? 'Resolved ✓' : 'Resolve')}
196
+ </Button>
197
+ <Button variant="ghost" size="sm" className={cn('h-6 px-2 text-[11px]', copied ? 'text-success' : 'text-muted-foreground')} onClick={copyLink}>
198
+ {copied ? 'Copied!' : 'Copy link'}
199
+ </Button>
200
+ <Button variant="ghost" size="icon" className="h-6 w-6 text-muted-foreground" aria-label="Close" onClick={onClose}>×</Button>
201
+ </div>
202
+ </div>
203
+
204
+ {/* Body */}
205
+ <div className="flex-auto overflow-y-auto px-3 pt-3 no-scrollbar">
206
+ {!editing ? (
207
+ <p className="text-sm leading-relaxed m-0 mb-2 break-words">{commentText}</p>
208
+ ) : (
209
+ <div className="mb-2">
210
+ <Textarea className="min-h-[40px] max-h-[100px] text-xs mb-2" value={editText} onChange={(e) => setEditText(e.target.value)} />
211
+ <div className="flex justify-end gap-1">
212
+ <Button variant="outline" size="sm" className="h-6 text-xs border border-input text-foreground" onClick={() => setEditing(false)}>Cancel</Button>
213
+ <Button size="sm" className="h-6 text-xs" disabled={saving} onClick={saveEdit}>{saving ? 'Saving…' : 'Save'}</Button>
214
+ </div>
215
+ </div>
216
+ )}
217
+
218
+ {/* Reactions */}
219
+ <div className="flex items-center flex-wrap gap-1 mb-2">
220
+ {reactions.map((group, gi) => (
221
+ (group.users?.totalCount ?? 0) > 0 && (
222
+ <button key={group.content} className={pillClass(group.viewerHasReacted)}
223
+ onClick={(e) => { e.stopPropagation(); toggleReaction(group.content, gi) }}>
224
+ <span className="mr-1">{emojiFor(group.content)}</span><span>{group.users?.totalCount ?? 0}</span>
225
+ </button>
226
+ )
227
+ ))}
228
+ <div className="relative">
229
+ <button className="inline-flex items-center text-xs px-2 py-0.5 rounded-full border border-border bg-transparent text-muted-foreground cursor-pointer hover:border-primary"
230
+ onClick={(e) => { e.stopPropagation(); setPickerTarget(pickerTarget === 'comment' ? null : 'comment') }}>😀 +</button>
231
+ {pickerTarget === 'comment' && (
232
+ <div className="absolute bottom-full left-0 mb-1 flex gap-0.5 p-1 bg-popover border border-border rounded-lg shadow-lg z-10">
233
+ {emojiEntries.map(([key, emoji]) => (
234
+ <button key={key} className={emojiPickerBtnClass(isReacted(key))}
235
+ onClick={(e) => { e.stopPropagation(); toggleReaction(key); setPickerTarget(null) }}>{emoji}</button>
236
+ ))}
237
+ </div>
238
+ )}
239
+ </div>
240
+ {!editing && canEdit && (
241
+ <button className="ml-auto text-xs text-muted-foreground bg-transparent border-none cursor-pointer hover:underline" onClick={() => { setEditing(true); setEditText(commentText) }}>Edit</button>
242
+ )}
243
+ </div>
244
+
245
+ {/* Replies */}
246
+ {replies.length > 0 && (
247
+ <>
248
+ <Separator className="my-2" />
249
+ <div className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">{replies.length} {replies.length === 1 ? 'Reply' : 'Replies'}</div>
250
+ {replies.map((reply, ri) => (
251
+ <div key={reply.id ?? ri} className="flex mb-2 gap-2">
252
+ {reply.author?.avatarUrl && (
253
+ <Avatar.Root className="h-5 w-5 shrink-0">
254
+ <Avatar.Image src={reply.author.avatarUrl} alt={reply.author?.login} />
255
+ <Avatar.Fallback className="text-[10px]">{(reply.author?.login ?? '?')[0]?.toUpperCase()}</Avatar.Fallback>
256
+ </Avatar.Root>
257
+ )}
258
+ <div className="flex-auto min-w-0">
259
+ <div className="flex items-start justify-between mb-0.5">
260
+ <div className="flex flex-col">
261
+ <span className="text-xs font-semibold">{reply.author?.login ?? 'unknown'}</span>
262
+ {reply.createdAt && <span className="text-[11px] text-muted-foreground leading-tight">{timeAgo(reply.createdAt)}</span>}
263
+ </div>
264
+ {user && reply.author?.login === user.login && (
265
+ <div className="flex gap-2 ml-auto shrink-0">
266
+ {editingReply !== ri && (
267
+ <>
268
+ <button className="text-[11px] text-muted-foreground bg-transparent border-none cursor-pointer hover:underline" onClick={() => { setEditingReply(ri); setEditReplyText(replyTexts[ri]) }}>Edit</button>
269
+ <button className="text-[11px] text-destructive bg-transparent border-none cursor-pointer hover:underline" onClick={() => deleteReplyAt(ri)}>Delete</button>
270
+ </>
271
+ )}
272
+ </div>
273
+ )}
274
+ </div>
275
+ {editingReply !== ri ? (
276
+ <p className="text-sm leading-relaxed m-0 break-words">{replyTexts[ri] ?? reply.text ?? reply.body}</p>
277
+ ) : (
278
+ <div>
279
+ <Textarea className="min-h-[40px] max-h-[100px] text-xs mb-1" value={editReplyText} onChange={(e) => setEditReplyText(e.target.value)} />
280
+ <div className="flex justify-end gap-1">
281
+ <Button variant="outline" size="sm" className="h-6 text-xs border border-input text-foreground" onClick={() => setEditingReply(-1)}>Cancel</Button>
282
+ <Button size="sm" className="h-6 text-xs" disabled={savingReply} onClick={() => saveReply(ri)}>{savingReply ? 'Saving…' : 'Save'}</Button>
283
+ </div>
284
+ </div>
285
+ )}
286
+ {/* Reply reactions */}
287
+ <div className="flex items-center flex-wrap gap-1 mt-1">
288
+ {(replyReactions[ri] ?? []).map((rg, rgi) => (
289
+ (rg.users?.totalCount ?? 0) > 0 && (
290
+ <button key={rg.content} className={pillClass(rg.viewerHasReacted)}
291
+ onClick={(e) => { e.stopPropagation(); toggleReplyReaction(ri, rg.content, rgi) }}>
292
+ <span className="mr-1">{emojiFor(rg.content)}</span><span>{rg.users?.totalCount ?? 0}</span>
293
+ </button>
294
+ )
295
+ ))}
296
+ <div className="relative">
297
+ <button className="inline-flex items-center text-xs px-2 py-0.5 rounded-full border border-border bg-transparent text-muted-foreground cursor-pointer"
298
+ onClick={(e) => { e.stopPropagation(); setPickerTarget(pickerTarget === `reply-${ri}` ? null : `reply-${ri}`) }}>😀 +</button>
299
+ {pickerTarget === `reply-${ri}` && (
300
+ <div className="absolute bottom-full left-0 mb-1 flex gap-0.5 p-1 bg-popover border border-border rounded-lg shadow-lg z-10">
301
+ {emojiEntries.map(([key, emoji]) => (
302
+ <button key={key} className={emojiPickerBtnClass(isReplyReacted(ri, key))}
303
+ onClick={(e) => { e.stopPropagation(); toggleReplyReaction(ri, key); setPickerTarget(null) }}>{emoji}</button>
304
+ ))}
305
+ </div>
306
+ )}
307
+ </div>
308
+ </div>
309
+ </div>
310
+ </div>
311
+ ))}
312
+ </>
313
+ )}
314
+ </div>
315
+
316
+ {/* Reply form */}
317
+ {canReply && (
318
+ <div className="border-t border-border px-3 py-3 flex flex-col">
319
+ <Textarea className="min-h-[40px] max-h-[100px] text-xs mb-1" placeholder="Reply…" value={replyText} onChange={(e) => setReplyText(e.target.value)} onKeyDown={handleReplyKeydown} onBlur={handleReplyBlur} />
320
+ <div className="flex justify-end mt-1">
321
+ <Button size="sm" className="text-xs" disabled={!replyText.trim() || submittingReply} onClick={submitReply}>
322
+ {submittingReply ? 'Posting…' : 'Reply'}
323
+ </Button>
324
+ </div>
325
+ </div>
326
+ )}
327
+ </div>
328
+ )
329
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * CommentsDrawer — right-side panel listing all comments across all routes.
3
+ * Uses shadcn Avatar, Badge, Button.
4
+ */
5
+
6
+ import { useState, useEffect } from 'react'
7
+ import { listDiscussions, fetchRouteDiscussion } from '../api.js'
8
+ import { getCommentsConfig } from '../config.js'
9
+ import * as Avatar from '../../lib/components/ui/avatar/index.js'
10
+ import { Badge } from '../../lib/components/ui/badge/index.js'
11
+ import { Button } from '../../lib/components/ui/button/index.js'
12
+
13
+ function timeAgo(dateStr) {
14
+ return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
15
+ }
16
+
17
+ export default function CommentsDrawer({ onClose, onNavigate }) {
18
+ const [loading, setLoading] = useState(true)
19
+ const [error, setError] = useState(null)
20
+ const [groups, setGroups] = useState([])
21
+
22
+ useEffect(() => {
23
+ let cancelled = false
24
+ async function load() {
25
+ try {
26
+ const discussions = await listDiscussions()
27
+ if (cancelled) return
28
+ if (!discussions || discussions.length === 0) { setLoading(false); return }
29
+ const basePath = getCommentsConfig()?.basePath ?? '/'
30
+ const result = []
31
+ for (const disc of discussions) {
32
+ const routeMatch = disc.title?.match(/^Comments:\s*(.+)$/)
33
+ if (!routeMatch) continue
34
+ const route = routeMatch[1]
35
+ if (!route.startsWith(basePath)) continue
36
+ let discussion
37
+ try { discussion = await fetchRouteDiscussion(route) } catch { continue }
38
+ if (!discussion?.comments?.length) continue
39
+ result.push({ route, comments: discussion.comments })
40
+ }
41
+ if (!cancelled) setGroups(result)
42
+ } catch (err) { if (!cancelled) setError(err.message) }
43
+ finally { if (!cancelled) setLoading(false) }
44
+ }
45
+ load()
46
+ return () => { cancelled = true }
47
+ }, [])
48
+
49
+ return (
50
+ <>
51
+ <div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
52
+ <h2 className="text-base font-semibold">All Comments</h2>
53
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground" aria-label="Close" onClick={onClose}>&#215;</Button>
54
+ </div>
55
+
56
+ <div className="flex-auto overflow-y-auto">
57
+ {loading && (
58
+ <div className="py-8 px-4 text-center text-sm text-muted-foreground">Loading comments&#8230;</div>
59
+ )}
60
+ {!loading && error && (
61
+ <div className="py-8 px-4 text-center text-sm text-muted-foreground">Failed to load comments: {error}</div>
62
+ )}
63
+ {!loading && !error && groups.length === 0 && (
64
+ <div className="py-8 px-4 text-center text-sm text-muted-foreground">No comments yet</div>
65
+ )}
66
+ {!loading && !error && groups.map((group) => (
67
+ <div key={group.route} className="border-b border-border">
68
+ <div className="flex items-center px-4 py-2 bg-muted/50 text-xs font-semibold text-muted-foreground">
69
+ <code className="text-primary">{group.route}</code>
70
+ <span className="ml-auto font-normal whitespace-nowrap">{group.comments.length} {group.comments.length !== 1 ? 'comments' : 'comment'}</span>
71
+ </div>
72
+ {group.comments.map((comment) => (
73
+ <button
74
+ key={comment.id}
75
+ className={`flex px-4 py-2 cursor-pointer border-none bg-transparent w-full text-left font-sans hover:bg-accent/50 transition-colors${comment.meta?.resolved ? ' opacity-60' : ''}`}
76
+ onClick={() => onNavigate?.(group.route, comment.id)}
77
+ >
78
+ {comment.author?.avatarUrl && (
79
+ <Avatar.Root className="h-6 w-6 shrink-0 mr-2">
80
+ <Avatar.Image src={comment.author.avatarUrl} alt={comment.author?.login ?? ''} />
81
+ <Avatar.Fallback className="text-[10px]">{(comment.author?.login ?? '?')[0]?.toUpperCase()}</Avatar.Fallback>
82
+ </Avatar.Root>
83
+ )}
84
+ <div className="flex flex-col flex-auto min-w-0 gap-0.5">
85
+ <div className="flex items-center gap-1">
86
+ <span className={`text-xs font-semibold ${comment.meta?.resolved ? 'text-muted-foreground' : 'text-foreground'}`}>{comment.author?.login ?? 'unknown'}</span>
87
+ {comment.createdAt && <span className="text-[11px] text-muted-foreground">{timeAgo(comment.createdAt)}</span>}
88
+ {comment.meta?.resolved && <Badge variant="outline" className="text-success text-[10px] px-1 py-0">Resolved</Badge>}
89
+ </div>
90
+ <p className={`text-sm leading-snug overflow-hidden whitespace-nowrap text-ellipsis m-0 ${comment.meta?.resolved ? 'text-muted-foreground' : 'text-foreground'}`}>{(comment.text ?? '').slice(0, 100)}</p>
91
+ {(comment.replies?.length ?? 0) > 0 && (
92
+ <div className="text-[11px] text-muted-foreground mt-0.5">&#128172; {comment.replies.length} {comment.replies.length === 1 ? 'reply' : 'replies'}</div>
93
+ )}
94
+ </div>
95
+ </button>
96
+ ))}
97
+ </div>
98
+ ))}
99
+ </div>
100
+ </>
101
+ )
102
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Composer — inline comment textarea at click position.
3
+ * Uses shadcn Button, Textarea, Avatar.
4
+ */
5
+
6
+ import { useState, useMemo } from 'react'
7
+ import { Button } from '../../lib/components/ui/button/index.js'
8
+ import { Textarea } from '../../lib/components/ui/textarea/index.js'
9
+ import * as Avatar from '../../lib/components/ui/avatar/index.js'
10
+ import { saveDraft, getDraft, clearDraft, composerDraftKey } from '../commentDrafts.js'
11
+
12
+ export default function Composer({ user = null, route = '', onCancel, onSubmit }) {
13
+ const draftKey = useMemo(() => composerDraftKey(route), [route])
14
+ const [text, setText] = useState(() => getDraft(draftKey)?.text ?? '')
15
+
16
+ function submit() {
17
+ const val = text.trim()
18
+ if (!val) return
19
+ onSubmit?.(val)
20
+ }
21
+
22
+ function cancel() {
23
+ onCancel?.()
24
+ }
25
+
26
+ function handleBlur() {
27
+ if (text.trim()) {
28
+ saveDraft(draftKey, { type: 'comment', text })
29
+ } else {
30
+ clearDraft(draftKey)
31
+ }
32
+ }
33
+
34
+ function handleKeydown(e) {
35
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); submit() }
36
+ }
37
+
38
+ return (
39
+ <div className="flex flex-col font-sans" onKeyDown={handleKeydown}>
40
+ {user && (
41
+ <div className="flex items-center px-3 pt-2 gap-2">
42
+ <Avatar.Root className="h-5 w-5">
43
+ <Avatar.Image src={user.avatarUrl} alt={user.login} />
44
+ <Avatar.Fallback className="text-[10px]">{user.login[0]?.toUpperCase()}</Avatar.Fallback>
45
+ </Avatar.Root>
46
+ <span className="text-xs text-muted-foreground font-medium">{user.login}</span>
47
+ </div>
48
+ )}
49
+ <div className="px-3 pt-3">
50
+ <Textarea
51
+ className="min-h-[60px] max-h-[160px] resize-y text-sm"
52
+ placeholder="Leave a comment…"
53
+ value={text}
54
+ onChange={(e) => setText(e.target.value)}
55
+ onBlur={handleBlur}
56
+ />
57
+ </div>
58
+ <div className="flex items-center justify-end p-3 gap-1">
59
+ <Button variant="outline" size="sm" className="border border-input text-foreground" onClick={cancel}>Cancel</Button>
60
+ <Button size="sm" onClick={submit}>Comment</Button>
61
+ </div>
62
+ </div>
63
+ )
64
+ }
@@ -2,7 +2,7 @@
2
2
  * Tests for authModal.js — PAT entry modal lifecycle.
3
3
  *
4
4
  * Tests the modal's resolve/reject/close behavior via the imperative API.
5
- * Runs under the svelte vitest config since authModal.js imports a .svelte component.
5
+ * Runs under the React vitest config since authModal.js imports a React component.
6
6
  */
7
7
 
8
8
  import { vi } from 'vitest'
@@ -1,13 +1,14 @@
1
1
  /**
2
- * Comment window — Svelte popup that shows a comment thread with replies and reactions.
2
+ * Comment window — React popup that shows a comment thread with replies and reactions.
3
3
  *
4
4
  * Opens when clicking a comment pin. Shows comment body, author, replies,
5
5
  * reply input, reactions, and supports drag-to-move.
6
6
  * Styled with Tachyons + sb-* custom classes for light/dark mode support.
7
7
  */
8
8
 
9
- import { mount, unmount } from 'svelte'
10
- import CommentWindowComponent from './CommentWindow.svelte'
9
+ import { createElement } from 'react'
10
+ import { createRoot } from 'react-dom/client'
11
+ import CommentWindowComponent from './CommentWindow.jsx'
11
12
  import { getCachedUser } from '../auth.js'
12
13
  import { saveDraft, replyDraftKey } from '../commentDrafts.js'
13
14
  import './comment-layout.css'
@@ -50,17 +51,17 @@ export function showCommentWindow(container, comment, discussion, callbacks = {}
50
51
 
51
52
  container.appendChild(win)
52
53
 
53
- let instance = null
54
+ let root = null
54
55
 
55
56
  function destroy() {
56
- // Save reply draft from DOM before Svelte unmounts
57
+ // Save reply draft from DOM before React unmounts
57
58
  const textarea = win.querySelector('textarea[placeholder="Reply…"]')
58
59
  const val = textarea?.value?.trim()
59
60
  if (val) {
60
61
  saveDraft(replyDraftKey(comment.id), { type: 'reply', text: textarea.value })
61
62
  }
62
63
 
63
- if (instance) { unmount(instance); instance = null }
64
+ if (root) { root.unmount(); root = null }
64
65
  win.remove()
65
66
  if (activeWindow?.el === win) activeWindow = null
66
67
  const currentUrl = new URL(window.location.href)
@@ -69,17 +70,15 @@ export function showCommentWindow(container, comment, discussion, callbacks = {}
69
70
  callbacks.onClose?.()
70
71
  }
71
72
 
72
- instance = mount(CommentWindowComponent, {
73
- target: win,
74
- props: {
75
- comment,
76
- discussion,
77
- user,
78
- winEl: win,
79
- onClose: destroy,
80
- onMove: () => callbacks.onMove?.(),
81
- },
82
- })
73
+ root = createRoot(win)
74
+ root.render(createElement(CommentWindowComponent, {
75
+ comment,
76
+ discussion,
77
+ user,
78
+ winEl: win,
79
+ onClose: destroy,
80
+ onMove: () => callbacks.onMove?.(),
81
+ }))
83
82
 
84
83
  // Adjust position to keep window within viewport
85
84
  requestAnimationFrame(() => {
@@ -1,13 +1,14 @@
1
1
  /**
2
- * Comments drawer — Svelte right-side panel listing all comments across all routes.
2
+ * Comments drawer — React right-side panel listing all comments across all routes.
3
3
  *
4
4
  * Opened from the DevTools "See all comments" menu item.
5
5
  * Clicking a comment navigates to its route and opens it.
6
6
  * Styled with Tachyons + sb-* custom classes for light/dark mode support.
7
7
  */
8
8
 
9
- import { mount, unmount } from 'svelte'
10
- import CommentsDrawerComponent from './CommentsDrawer.svelte'
9
+ import { createElement } from 'react'
10
+ import { createRoot } from 'react-dom/client'
11
+ import CommentsDrawerComponent from './CommentsDrawer.jsx'
11
12
  import { isAuthenticated } from '../auth.js'
12
13
  import { setCommentMode } from '../commentMode.js'
13
14
  import './comment-layout.css'
@@ -32,7 +33,7 @@ export async function openCommentsDrawer() {
32
33
  document.body.appendChild(backdrop)
33
34
  document.body.appendChild(drawer)
34
35
 
35
- let instance = null
36
+ let root = null
36
37
 
37
38
  function onKeyDown(e) {
38
39
  if (e.key === 'Escape') {
@@ -44,28 +45,26 @@ export async function openCommentsDrawer() {
44
45
  }
45
46
  window.addEventListener('keydown', onKeyDown, true)
46
47
 
47
- instance = mount(CommentsDrawerComponent, {
48
- target: drawer,
49
- props: {
50
- onClose: closeCommentsDrawer,
51
- onNavigate: (route, commentId) => {
52
- closeCommentsDrawer()
53
- if (window.location.pathname !== route) {
54
- const navUrl = new URL(window.location.href)
55
- navUrl.pathname = route
56
- navUrl.searchParams.set('comment', commentId)
57
- window.location.href = navUrl.toString()
58
- } else {
59
- const navUrl = new URL(window.location.href)
60
- navUrl.searchParams.set('comment', commentId)
61
- window.history.replaceState(null, '', navUrl.toString())
62
- setCommentMode(true)
63
- }
64
- },
48
+ root = createRoot(drawer)
49
+ root.render(createElement(CommentsDrawerComponent, {
50
+ onClose: closeCommentsDrawer,
51
+ onNavigate: (route, commentId) => {
52
+ closeCommentsDrawer()
53
+ if (window.location.pathname !== route) {
54
+ const navUrl = new URL(window.location.href)
55
+ navUrl.pathname = route
56
+ navUrl.searchParams.set('comment', commentId)
57
+ window.location.href = navUrl.toString()
58
+ } else {
59
+ const navUrl = new URL(window.location.href)
60
+ navUrl.searchParams.set('comment', commentId)
61
+ window.history.replaceState(null, '', navUrl.toString())
62
+ setCommentMode(true)
63
+ }
65
64
  },
66
- })
65
+ }))
67
66
 
68
- activeDrawer = { backdrop, drawer, instance, onKeyDown }
67
+ activeDrawer = { backdrop, drawer, root, onKeyDown }
69
68
  }
70
69
 
71
70
  /**
@@ -76,8 +75,8 @@ export function closeCommentsDrawer() {
76
75
  if (activeDrawer.onKeyDown) {
77
76
  window.removeEventListener('keydown', activeDrawer.onKeyDown, true)
78
77
  }
79
- if (activeDrawer.instance) {
80
- unmount(activeDrawer.instance)
78
+ if (activeDrawer.root) {
79
+ activeDrawer.root.unmount()
81
80
  }
82
81
  activeDrawer.backdrop.remove()
83
82
  activeDrawer.drawer.remove()