@dfosco/storyboard 0.5.0-alpha.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 (592) hide show
  1. package/commandpalette.config.json +152 -0
  2. package/dist/storyboard-ui.css +1 -0
  3. package/dist/storyboard-ui.js +21328 -0
  4. package/dist/storyboard-ui.js.map +1 -0
  5. package/dist/tailwind.css +2 -0
  6. package/dist/tiny-canvas.css +1 -0
  7. package/dist/tiny-canvas.js +389 -0
  8. package/package.json +121 -0
  9. package/paste.config.json +67 -0
  10. package/scaffold/AGENTS.md +432 -0
  11. package/scaffold/agents/prompt-agent.agent.md +181 -0
  12. package/scaffold/agents/terminal-agent.agent.md +351 -0
  13. package/scaffold/codex/config.toml +246 -0
  14. package/scaffold/deploy.yml +103 -0
  15. package/scaffold/githooks/pre-push +114 -0
  16. package/scaffold/gitignore +64 -0
  17. package/scaffold/manifest.json +56 -0
  18. package/scaffold/preview.yml +181 -0
  19. package/scaffold/scripts/link.sh +26 -0
  20. package/scaffold/scripts/unlink.sh +10 -0
  21. package/scaffold/skills/agent-browser/SKILL.md +260 -0
  22. package/scaffold/skills/canvas/SKILL.md +364 -0
  23. package/scaffold/skills/create/SKILL.md +501 -0
  24. package/scaffold/skills/ship/SKILL.md +237 -0
  25. package/scaffold/skills/storyboard/SKILL.md +360 -0
  26. package/scaffold/skills/update-storyboard/SKILL.md +16 -0
  27. package/scaffold/skills/update-storyboard/update-storyboard-packages.sh +26 -0
  28. package/scaffold/skills/vitest/GENERATION.md +5 -0
  29. package/scaffold/skills/vitest/SKILL.md +52 -0
  30. package/scaffold/skills/vitest/references/advanced-environments.md +264 -0
  31. package/scaffold/skills/vitest/references/advanced-projects.md +300 -0
  32. package/scaffold/skills/vitest/references/advanced-type-testing.md +237 -0
  33. package/scaffold/skills/vitest/references/advanced-vi.md +249 -0
  34. package/scaffold/skills/vitest/references/core-cli.md +166 -0
  35. package/scaffold/skills/vitest/references/core-config.md +174 -0
  36. package/scaffold/skills/vitest/references/core-describe.md +193 -0
  37. package/scaffold/skills/vitest/references/core-expect.md +219 -0
  38. package/scaffold/skills/vitest/references/core-hooks.md +244 -0
  39. package/scaffold/skills/vitest/references/core-test-api.md +233 -0
  40. package/scaffold/skills/vitest/references/features-concurrency.md +250 -0
  41. package/scaffold/skills/vitest/references/features-context.md +238 -0
  42. package/scaffold/skills/vitest/references/features-coverage.md +207 -0
  43. package/scaffold/skills/vitest/references/features-filtering.md +211 -0
  44. package/scaffold/skills/vitest/references/features-mocking.md +265 -0
  45. package/scaffold/skills/vitest/references/features-snapshots.md +207 -0
  46. package/scaffold/skills/worktree/SKILL.md +93 -0
  47. package/scaffold/storyboard.config.json +44 -0
  48. package/src/canvas/Canvas.jsx +78 -0
  49. package/src/canvas/Draggable.jsx +235 -0
  50. package/src/canvas/index.d.ts +41 -0
  51. package/src/canvas/index.js +6 -0
  52. package/src/canvas/style.css +118 -0
  53. package/src/canvas/useResetCanvas.js +17 -0
  54. package/src/canvas/utils.js +136 -0
  55. package/src/core/assets/fonts/IoskeleyMono-Bold.woff2 +0 -0
  56. package/src/core/assets/fonts/IoskeleyMono-Italic.woff2 +0 -0
  57. package/src/core/assets/fonts/IoskeleyMono-Medium.woff2 +0 -0
  58. package/src/core/assets/fonts/IoskeleyMono-Regular.woff2 +0 -0
  59. package/src/core/assets/fonts/IoskeleyMono-SemiBold.woff2 +0 -0
  60. package/src/core/autosync/server.js +714 -0
  61. package/src/core/autosync/server.test.js +158 -0
  62. package/src/core/canvas/__tests__/agent-integration.test.js +596 -0
  63. package/src/core/canvas/__tests__/helpers/browser.js +95 -0
  64. package/src/core/canvas/__tests__/helpers/canvas-api.js +129 -0
  65. package/src/core/canvas/__tests__/helpers/perf.js +118 -0
  66. package/src/core/canvas/__tests__/helpers/setup.js +176 -0
  67. package/src/core/canvas/__tests__/helpers/tmux.js +130 -0
  68. package/src/core/canvas/__tests__/helpers/transcript.js +132 -0
  69. package/src/core/canvas/__tests__/terminal-integration.test.js +177 -0
  70. package/src/core/canvas/collision.js +292 -0
  71. package/src/core/canvas/collision.test.js +371 -0
  72. package/src/core/canvas/compact.js +83 -0
  73. package/src/core/canvas/deriveCanvasId.test.js +40 -0
  74. package/src/core/canvas/githubEmbeds.js +527 -0
  75. package/src/core/canvas/githubEmbeds.test.js +302 -0
  76. package/src/core/canvas/hot-pool.js +766 -0
  77. package/src/core/canvas/identity.js +107 -0
  78. package/src/core/canvas/identity.test.js +100 -0
  79. package/src/core/canvas/materializer.js +259 -0
  80. package/src/core/canvas/materializer.test.js +356 -0
  81. package/src/core/canvas/selectedWidgets.js +270 -0
  82. package/src/core/canvas/selectedWidgets.test.js +321 -0
  83. package/src/core/canvas/server.js +3134 -0
  84. package/src/core/canvas/server.test.js +379 -0
  85. package/src/core/canvas/terminal-config.js +330 -0
  86. package/src/core/canvas/terminal-registry.js +465 -0
  87. package/src/core/canvas/terminal-server.js +1436 -0
  88. package/src/core/canvas/writeGuard.js +53 -0
  89. package/src/core/cli/agent.js +85 -0
  90. package/src/core/cli/branch.js +386 -0
  91. package/src/core/cli/canvasAdd.js +241 -0
  92. package/src/core/cli/canvasBatch.js +98 -0
  93. package/src/core/cli/canvasBounds.js +160 -0
  94. package/src/core/cli/canvasRead.js +236 -0
  95. package/src/core/cli/canvasUpdate.js +179 -0
  96. package/src/core/cli/code.js +67 -0
  97. package/src/core/cli/compact.js +62 -0
  98. package/src/core/cli/create.js +674 -0
  99. package/src/core/cli/dev-helpers.js +53 -0
  100. package/src/core/cli/dev-helpers.test.js +53 -0
  101. package/src/core/cli/dev.js +430 -0
  102. package/src/core/cli/exit.js +38 -0
  103. package/src/core/cli/flags.js +174 -0
  104. package/src/core/cli/flags.test.js +155 -0
  105. package/src/core/cli/index.js +233 -0
  106. package/src/core/cli/intro.js +37 -0
  107. package/src/core/cli/proxy.js +319 -0
  108. package/src/core/cli/proxy.test.js +63 -0
  109. package/src/core/cli/schemas.js +223 -0
  110. package/src/core/cli/server.js +192 -0
  111. package/src/core/cli/serverUrl.js +61 -0
  112. package/src/core/cli/sessions.js +459 -0
  113. package/src/core/cli/setup.js +404 -0
  114. package/src/core/cli/terminal-commands.js +287 -0
  115. package/src/core/cli/terminal-messaging.js +231 -0
  116. package/src/core/cli/terminal-welcome.js +515 -0
  117. package/src/core/cli/updateVersion.js +124 -0
  118. package/src/core/comments/api.js +284 -0
  119. package/src/core/comments/api.test.js +282 -0
  120. package/src/core/comments/auth.js +151 -0
  121. package/src/core/comments/auth.test.js +167 -0
  122. package/src/core/comments/commentCache.js +109 -0
  123. package/src/core/comments/commentCache.test.js +48 -0
  124. package/src/core/comments/commentDrafts.js +68 -0
  125. package/src/core/comments/commentMode.js +63 -0
  126. package/src/core/comments/commentMode.test.js +90 -0
  127. package/src/core/comments/config.js +47 -0
  128. package/src/core/comments/config.test.js +77 -0
  129. package/src/core/comments/graphql.js +65 -0
  130. package/src/core/comments/graphql.test.js +95 -0
  131. package/src/core/comments/index.js +42 -0
  132. package/src/core/comments/metadata.js +52 -0
  133. package/src/core/comments/metadata.test.js +110 -0
  134. package/src/core/comments/queries.js +245 -0
  135. package/src/core/comments/ui/AuthModal.jsx +114 -0
  136. package/src/core/comments/ui/CommentOverlay.js +52 -0
  137. package/src/core/comments/ui/CommentWindow.jsx +329 -0
  138. package/src/core/comments/ui/CommentsDrawer.jsx +102 -0
  139. package/src/core/comments/ui/Composer.jsx +64 -0
  140. package/src/core/comments/ui/authModal.js +66 -0
  141. package/src/core/comments/ui/authModal.test.js +76 -0
  142. package/src/core/comments/ui/comment-cursor-dark.svg +1 -0
  143. package/src/core/comments/ui/comment-cursor.svg +1 -0
  144. package/src/core/comments/ui/comment-layout.css +142 -0
  145. package/src/core/comments/ui/commentWindow.js +121 -0
  146. package/src/core/comments/ui/comments.css +242 -0
  147. package/src/core/comments/ui/commentsDrawer.js +84 -0
  148. package/src/core/comments/ui/composer.js +136 -0
  149. package/src/core/comments/ui/index.js +14 -0
  150. package/src/core/comments/ui/mount.js +687 -0
  151. package/src/core/comments/ui/mount.test.js +336 -0
  152. package/src/core/data/dotPath.js +53 -0
  153. package/src/core/data/dotPath.test.js +114 -0
  154. package/src/core/data/loader.js +409 -0
  155. package/src/core/data/loader.test.js +599 -0
  156. package/src/core/data/viewfinder.js +363 -0
  157. package/src/core/data/viewfinder.test.js +456 -0
  158. package/src/core/devtools/devtools-consumer.js +28 -0
  159. package/src/core/devtools/devtools.js +144 -0
  160. package/src/core/devtools/devtools.test.js +75 -0
  161. package/src/core/devtools/sceneDebug.js +112 -0
  162. package/src/core/devtools/sceneDebug.test.js +141 -0
  163. package/src/core/index.js +124 -0
  164. package/src/core/inspector/fiberWalker.js +239 -0
  165. package/src/core/inspector/highlighter.js +275 -0
  166. package/src/core/inspector/mouseMode.js +259 -0
  167. package/src/core/lib/components/ui/alert/alert-action.jsx +11 -0
  168. package/src/core/lib/components/ui/alert/alert-description.jsx +11 -0
  169. package/src/core/lib/components/ui/alert/alert-title.jsx +11 -0
  170. package/src/core/lib/components/ui/alert/alert.jsx +25 -0
  171. package/src/core/lib/components/ui/alert/index.js +17 -0
  172. package/src/core/lib/components/ui/avatar/avatar-badge.jsx +22 -0
  173. package/src/core/lib/components/ui/avatar/avatar-fallback.jsx +18 -0
  174. package/src/core/lib/components/ui/avatar/avatar-group-count.jsx +19 -0
  175. package/src/core/lib/components/ui/avatar/avatar-group.jsx +19 -0
  176. package/src/core/lib/components/ui/avatar/avatar-image.jsx +15 -0
  177. package/src/core/lib/components/ui/avatar/avatar.jsx +19 -0
  178. package/src/core/lib/components/ui/avatar/index.js +22 -0
  179. package/src/core/lib/components/ui/badge/badge.jsx +31 -0
  180. package/src/core/lib/components/ui/badge/index.js +2 -0
  181. package/src/core/lib/components/ui/button/button.jsx +100 -0
  182. package/src/core/lib/components/ui/button/index.js +12 -0
  183. package/src/core/lib/components/ui/card/card-action.jsx +11 -0
  184. package/src/core/lib/components/ui/card/card-content.jsx +11 -0
  185. package/src/core/lib/components/ui/card/card-description.jsx +11 -0
  186. package/src/core/lib/components/ui/card/card-footer.jsx +11 -0
  187. package/src/core/lib/components/ui/card/card-header.jsx +19 -0
  188. package/src/core/lib/components/ui/card/card-title.jsx +11 -0
  189. package/src/core/lib/components/ui/card/card.jsx +17 -0
  190. package/src/core/lib/components/ui/card/index.js +25 -0
  191. package/src/core/lib/components/ui/checkbox/checkbox.jsx +29 -0
  192. package/src/core/lib/components/ui/checkbox/index.js +6 -0
  193. package/src/core/lib/components/ui/collapsible/collapsible-content.jsx +7 -0
  194. package/src/core/lib/components/ui/collapsible/collapsible-trigger.jsx +7 -0
  195. package/src/core/lib/components/ui/collapsible/collapsible.jsx +7 -0
  196. package/src/core/lib/components/ui/collapsible/index.js +13 -0
  197. package/src/core/lib/components/ui/dialog/dialog-close.jsx +7 -0
  198. package/src/core/lib/components/ui/dialog/dialog-content.jsx +34 -0
  199. package/src/core/lib/components/ui/dialog/dialog-description.jsx +15 -0
  200. package/src/core/lib/components/ui/dialog/dialog-footer.jsx +23 -0
  201. package/src/core/lib/components/ui/dialog/dialog-header.jsx +11 -0
  202. package/src/core/lib/components/ui/dialog/dialog-overlay.jsx +15 -0
  203. package/src/core/lib/components/ui/dialog/dialog-portal.jsx +4 -0
  204. package/src/core/lib/components/ui/dialog/dialog-title.jsx +15 -0
  205. package/src/core/lib/components/ui/dialog/dialog-trigger.jsx +7 -0
  206. package/src/core/lib/components/ui/dialog/dialog.jsx +4 -0
  207. package/src/core/lib/components/ui/dialog/index.js +34 -0
  208. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +8 -0
  209. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +30 -0
  210. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +22 -0
  211. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +16 -0
  212. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +7 -0
  213. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +20 -0
  214. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-label.jsx +17 -0
  215. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +4 -0
  216. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +7 -0
  217. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +29 -0
  218. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +15 -0
  219. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.jsx +16 -0
  220. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +15 -0
  221. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +23 -0
  222. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +4 -0
  223. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +7 -0
  224. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu.jsx +4 -0
  225. package/src/core/lib/components/ui/dropdown-menu/index.js +54 -0
  226. package/src/core/lib/components/ui/input/index.js +7 -0
  227. package/src/core/lib/components/ui/input/input.jsx +19 -0
  228. package/src/core/lib/components/ui/label/index.js +7 -0
  229. package/src/core/lib/components/ui/label/label.jsx +19 -0
  230. package/src/core/lib/components/ui/panel/index.js +24 -0
  231. package/src/core/lib/components/ui/panel/panel-body.jsx +11 -0
  232. package/src/core/lib/components/ui/panel/panel-close.jsx +16 -0
  233. package/src/core/lib/components/ui/panel/panel-content.jsx +29 -0
  234. package/src/core/lib/components/ui/panel/panel-footer.jsx +11 -0
  235. package/src/core/lib/components/ui/panel/panel-header.jsx +11 -0
  236. package/src/core/lib/components/ui/panel/panel-title.jsx +12 -0
  237. package/src/core/lib/components/ui/panel/panel.jsx +4 -0
  238. package/src/core/lib/components/ui/popover/index.js +28 -0
  239. package/src/core/lib/components/ui/popover/popover-close.jsx +7 -0
  240. package/src/core/lib/components/ui/popover/popover-content.jsx +22 -0
  241. package/src/core/lib/components/ui/popover/popover-description.jsx +11 -0
  242. package/src/core/lib/components/ui/popover/popover-header.jsx +11 -0
  243. package/src/core/lib/components/ui/popover/popover-portal.jsx +4 -0
  244. package/src/core/lib/components/ui/popover/popover-title.jsx +11 -0
  245. package/src/core/lib/components/ui/popover/popover-trigger.jsx +8 -0
  246. package/src/core/lib/components/ui/popover/popover.jsx +4 -0
  247. package/src/core/lib/components/ui/searchable-list.jsx +160 -0
  248. package/src/core/lib/components/ui/select/index.js +37 -0
  249. package/src/core/lib/components/ui/select/select-content.jsx +30 -0
  250. package/src/core/lib/components/ui/select/select-group-heading.jsx +17 -0
  251. package/src/core/lib/components/ui/select/select-group.jsx +15 -0
  252. package/src/core/lib/components/ui/select/select-item.jsx +26 -0
  253. package/src/core/lib/components/ui/select/select-label.jsx +11 -0
  254. package/src/core/lib/components/ui/select/select-portal.jsx +4 -0
  255. package/src/core/lib/components/ui/select/select-scroll-down-button.jsx +18 -0
  256. package/src/core/lib/components/ui/select/select-scroll-up-button.jsx +18 -0
  257. package/src/core/lib/components/ui/select/select-separator.jsx +15 -0
  258. package/src/core/lib/components/ui/select/select-trigger.jsx +25 -0
  259. package/src/core/lib/components/ui/select/select.jsx +4 -0
  260. package/src/core/lib/components/ui/separator/index.js +7 -0
  261. package/src/core/lib/components/ui/separator/separator.jsx +22 -0
  262. package/src/core/lib/components/ui/sheet/index.js +34 -0
  263. package/src/core/lib/components/ui/sheet/sheet-close.jsx +7 -0
  264. package/src/core/lib/components/ui/sheet/sheet-content.jsx +35 -0
  265. package/src/core/lib/components/ui/sheet/sheet-description.jsx +15 -0
  266. package/src/core/lib/components/ui/sheet/sheet-footer.jsx +11 -0
  267. package/src/core/lib/components/ui/sheet/sheet-header.jsx +11 -0
  268. package/src/core/lib/components/ui/sheet/sheet-overlay.jsx +15 -0
  269. package/src/core/lib/components/ui/sheet/sheet-portal.jsx +4 -0
  270. package/src/core/lib/components/ui/sheet/sheet-title.jsx +15 -0
  271. package/src/core/lib/components/ui/sheet/sheet-trigger.jsx +7 -0
  272. package/src/core/lib/components/ui/sheet/sheet.jsx +4 -0
  273. package/src/core/lib/components/ui/textarea/index.js +7 -0
  274. package/src/core/lib/components/ui/textarea/textarea.jsx +18 -0
  275. package/src/core/lib/components/ui/toggle/index.js +8 -0
  276. package/src/core/lib/components/ui/toggle/toggle.jsx +36 -0
  277. package/src/core/lib/components/ui/toggle-group/index.js +10 -0
  278. package/src/core/lib/components/ui/toggle-group/toggle-group-item.jsx +29 -0
  279. package/src/core/lib/components/ui/toggle-group/toggle-group.jsx +43 -0
  280. package/src/core/lib/components/ui/tooltip/index.js +3 -0
  281. package/src/core/lib/components/ui/tooltip/tooltip-content.jsx +21 -0
  282. package/src/core/lib/components/ui/tooltip/tooltip-trigger.jsx +23 -0
  283. package/src/core/lib/components/ui/tooltip/tooltip.jsx +11 -0
  284. package/src/core/lib/components/ui/trigger-button/index.js +6 -0
  285. package/src/core/lib/components/ui/trigger-button/trigger-button.css +38 -0
  286. package/src/core/lib/components/ui/trigger-button/trigger-button.jsx +63 -0
  287. package/src/core/lib/utils/index.js +6 -0
  288. package/src/core/logger/devLogger.js +238 -0
  289. package/src/core/logger/devLogger.test.js +193 -0
  290. package/src/core/modes/modes.css +98 -0
  291. package/src/core/modes/modes.js +492 -0
  292. package/src/core/modes/modes.test.js +562 -0
  293. package/src/core/mountStoryboardCore.js +478 -0
  294. package/src/core/rename-watcher/config.json +23 -0
  295. package/src/core/rename-watcher/watcher.js +531 -0
  296. package/src/core/scaffold.js +100 -0
  297. package/src/core/server/index.js +391 -0
  298. package/src/core/session/bodyClasses.js +128 -0
  299. package/src/core/session/bodyClasses.test.js +192 -0
  300. package/src/core/session/hashSubscribe.js +19 -0
  301. package/src/core/session/hashSubscribe.test.js +62 -0
  302. package/src/core/session/hideMode.js +424 -0
  303. package/src/core/session/hideMode.test.js +268 -0
  304. package/src/core/session/interceptHideParams.js +35 -0
  305. package/src/core/session/interceptHideParams.test.js +90 -0
  306. package/src/core/session/localStorage.js +134 -0
  307. package/src/core/session/localStorage.test.js +148 -0
  308. package/src/core/session/session.js +76 -0
  309. package/src/core/session/session.test.js +91 -0
  310. package/src/core/stores/canvasConfig.js +134 -0
  311. package/src/core/stores/canvasConfig.test.js +120 -0
  312. package/src/core/stores/commandActions.js +284 -0
  313. package/src/core/stores/commandPaletteConfig.js +31 -0
  314. package/src/core/stores/configSchema.js +232 -0
  315. package/src/core/stores/configSchema.test.js +72 -0
  316. package/src/core/stores/configStore.js +161 -0
  317. package/src/core/stores/customerModeConfig.js +30 -0
  318. package/src/core/stores/featureFlags.js +127 -0
  319. package/src/core/stores/paletteProviders.js +360 -0
  320. package/src/core/stores/paletteProviders.test.js +186 -0
  321. package/src/core/stores/plugins.js +40 -0
  322. package/src/core/stores/plugins.test.js +68 -0
  323. package/src/core/stores/recentArtifacts.js +68 -0
  324. package/src/core/stores/recentArtifacts.test.js +71 -0
  325. package/src/core/stores/sidePanelStore.ts +143 -0
  326. package/src/core/stores/themeStore.ts +291 -0
  327. package/src/core/stores/toolRegistry.js +227 -0
  328. package/src/core/stores/toolStateStore.js +183 -0
  329. package/src/core/stores/toolStateStore.test.js +220 -0
  330. package/src/core/stores/toolbarConfigStore.js +165 -0
  331. package/src/core/stores/uiConfig.js +64 -0
  332. package/src/core/stores/uiConfig.test.js +63 -0
  333. package/src/core/styles/tailwind.css +204 -0
  334. package/src/core/tools/handlers/autosync.js +12 -0
  335. package/src/core/tools/handlers/canvasAddWidget.js +11 -0
  336. package/src/core/tools/handlers/canvasAgents.js +20 -0
  337. package/src/core/tools/handlers/canvasToolbar.js +56 -0
  338. package/src/core/tools/handlers/commandPalette.js +9 -0
  339. package/src/core/tools/handlers/comments.js +16 -0
  340. package/src/core/tools/handlers/create.js +39 -0
  341. package/src/core/tools/handlers/devtools.js +122 -0
  342. package/src/core/tools/handlers/devtools.test.js +87 -0
  343. package/src/core/tools/handlers/featureFlags.js +21 -0
  344. package/src/core/tools/handlers/flows.js +68 -0
  345. package/src/core/tools/handlers/hideChrome.js +9 -0
  346. package/src/core/tools/handlers/hideToolbars.js +25 -0
  347. package/src/core/tools/handlers/inspector.js +19 -0
  348. package/src/core/tools/handlers/paletteTheme.js +35 -0
  349. package/src/core/tools/handlers/theme.js +9 -0
  350. package/src/core/tools/registry.js +26 -0
  351. package/src/core/tools/surfaces/canvasToolbar.js +10 -0
  352. package/src/core/tools/surfaces/commandList.js +10 -0
  353. package/src/core/tools/surfaces/mainToolbar.js +11 -0
  354. package/src/core/tools/surfaces/registry.js +19 -0
  355. package/src/core/ui/ActionMenuButton.jsx +114 -0
  356. package/src/core/ui/AutosyncMenuButton.css +67 -0
  357. package/src/core/ui/AutosyncMenuButton.jsx +242 -0
  358. package/src/core/ui/BranchSelect.jsx +29 -0
  359. package/src/core/ui/BranchSelect.module.css +30 -0
  360. package/src/core/ui/CanvasAgentsMenu.jsx +89 -0
  361. package/src/core/ui/CanvasCreateMenu.jsx +611 -0
  362. package/src/core/ui/CanvasSnap.css +27 -0
  363. package/src/core/ui/CanvasSnap.jsx +51 -0
  364. package/src/core/ui/CanvasUndoRedo.css +36 -0
  365. package/src/core/ui/CanvasUndoRedo.jsx +62 -0
  366. package/src/core/ui/CanvasZoomControl.css +53 -0
  367. package/src/core/ui/CanvasZoomControl.jsx +49 -0
  368. package/src/core/ui/CanvasZoomToFit.css +18 -0
  369. package/src/core/ui/CanvasZoomToFit.jsx +26 -0
  370. package/src/core/ui/CommandMenu.css +8 -0
  371. package/src/core/ui/CommandMenu.jsx +287 -0
  372. package/src/core/ui/CommandPalette.jsx +35 -0
  373. package/src/core/ui/CommandPaletteTrigger.jsx +25 -0
  374. package/src/core/ui/CommentsMenuButton.jsx +40 -0
  375. package/src/core/ui/CoreUIBar.css +47 -0
  376. package/src/core/ui/CoreUIBar.jsx +905 -0
  377. package/src/core/ui/CreateMenuButton.jsx +117 -0
  378. package/src/core/ui/HideChromeTrigger.jsx +48 -0
  379. package/src/core/ui/Icon.jsx +279 -0
  380. package/src/core/ui/InspectorPanel.css +109 -0
  381. package/src/core/ui/InspectorPanel.jsx +632 -0
  382. package/src/core/ui/PwaInstallBanner.css +42 -0
  383. package/src/core/ui/PwaInstallBanner.jsx +124 -0
  384. package/src/core/ui/SidePanel.jsx +261 -0
  385. package/src/core/ui/ThemeMenuButton.jsx +139 -0
  386. package/src/core/ui/core-ui-colors.css +129 -0
  387. package/src/core/ui/design-modes.ts +7 -0
  388. package/src/core/ui/sidepanel.css +301 -0
  389. package/src/core/ui/viewfinder.ts +7 -0
  390. package/src/core/ui-entry.js +30 -0
  391. package/src/core/utils/fuzzySearch.js +117 -0
  392. package/src/core/utils/fuzzySearch.test.js +119 -0
  393. package/src/core/utils/mobileViewport.js +57 -0
  394. package/src/core/utils/mobileViewport.test.js +68 -0
  395. package/src/core/utils/prodMode.js +38 -0
  396. package/src/core/utils/smoothCorners.js +20 -0
  397. package/src/core/vite/docs-handler.js +155 -0
  398. package/src/core/vite/server-plugin.js +797 -0
  399. package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +260 -0
  400. package/src/core/workshop/features/createCanvas/index.js +14 -0
  401. package/src/core/workshop/features/createFlow/CreateFlowForm.jsx +334 -0
  402. package/src/core/workshop/features/createFlow/index.js +19 -0
  403. package/src/core/workshop/features/createFlow/server.js +663 -0
  404. package/src/core/workshop/features/createPage/CreatePageForm.jsx +304 -0
  405. package/src/core/workshop/features/createPage/index.js +11 -0
  406. package/src/core/workshop/features/createPrototype/CreatePrototypeForm.jsx +289 -0
  407. package/src/core/workshop/features/createPrototype/index.js +19 -0
  408. package/src/core/workshop/features/createPrototype/server.js +433 -0
  409. package/src/core/workshop/features/createStory/CreateStoryForm.jsx +208 -0
  410. package/src/core/workshop/features/createStory/index.js +14 -0
  411. package/src/core/workshop/features/registry-server.js +22 -0
  412. package/src/core/workshop/features/registry.js +28 -0
  413. package/src/core/workshop/features/templateIndex.js +155 -0
  414. package/src/core/workshop/ui/WorkshopPanel.jsx +98 -0
  415. package/src/core/workshop/ui/mount.ts +6 -0
  416. package/src/core/worktree/port.js +268 -0
  417. package/src/core/worktree/port.test.js +222 -0
  418. package/src/core/worktree/serverRegistry.js +120 -0
  419. package/src/internals/AuthModal/AuthModal.jsx +132 -0
  420. package/src/internals/AuthModal/AuthModal.module.css +221 -0
  421. package/src/internals/BranchBar/BranchBar.jsx +87 -0
  422. package/src/internals/BranchBar/BranchBar.module.css +247 -0
  423. package/src/internals/BranchBar/useBranches.js +93 -0
  424. package/src/internals/BranchBar/useBranches.test.js +68 -0
  425. package/src/internals/CommandPalette/CommandPalette.jsx +1361 -0
  426. package/src/internals/CommandPalette/CreateDialog.jsx +219 -0
  427. package/src/internals/CommandPalette/command-palette.css +180 -0
  428. package/src/internals/FlowError.module.css +30 -0
  429. package/src/internals/Icon.jsx +279 -0
  430. package/src/internals/StoryboardContext.js +3 -0
  431. package/src/internals/Viewfinder.jsx +1479 -0
  432. package/src/internals/Viewfinder.module.css +1540 -0
  433. package/src/internals/Workspace.jsx +7 -0
  434. package/src/internals/__mocks__/virtual-storyboard-data-index.js +4 -0
  435. package/src/internals/canvas/CanvasControls.jsx +112 -0
  436. package/src/internals/canvas/CanvasControls.module.css +135 -0
  437. package/src/internals/canvas/CanvasPage.bridge.test.jsx +387 -0
  438. package/src/internals/canvas/CanvasPage.dragdrop.test.jsx +350 -0
  439. package/src/internals/canvas/CanvasPage.jsx +3092 -0
  440. package/src/internals/canvas/CanvasPage.module.css +187 -0
  441. package/src/internals/canvas/CanvasPage.multiselect.test.jsx +358 -0
  442. package/src/internals/canvas/CanvasToolbar.jsx +73 -0
  443. package/src/internals/canvas/CanvasToolbar.module.css +92 -0
  444. package/src/internals/canvas/ComponentErrorBoundary.jsx +50 -0
  445. package/src/internals/canvas/ConnectorLayer.jsx +208 -0
  446. package/src/internals/canvas/ConnectorLayer.module.css +129 -0
  447. package/src/internals/canvas/MarqueeOverlay.jsx +20 -0
  448. package/src/internals/canvas/PageSelector.jsx +587 -0
  449. package/src/internals/canvas/PageSelector.module.css +261 -0
  450. package/src/internals/canvas/PageSelector.test.jsx +113 -0
  451. package/src/internals/canvas/WebGLContextPool.jsx +292 -0
  452. package/src/internals/canvas/WebGLContextPool.test.jsx +165 -0
  453. package/src/internals/canvas/canvasApi.js +164 -0
  454. package/src/internals/canvas/canvasReloadGuard.js +37 -0
  455. package/src/internals/canvas/canvasReloadGuard.test.js +27 -0
  456. package/src/internals/canvas/canvasTheme.js +118 -0
  457. package/src/internals/canvas/componentIsolate.jsx +165 -0
  458. package/src/internals/canvas/componentSetIsolate.jsx +257 -0
  459. package/src/internals/canvas/computeCanvasBounds.test.js +121 -0
  460. package/src/internals/canvas/connectorGeometry.js +132 -0
  461. package/src/internals/canvas/hotPoolDevLogs.js +25 -0
  462. package/src/internals/canvas/textSelection.js +10 -0
  463. package/src/internals/canvas/textSelection.test.js +26 -0
  464. package/src/internals/canvas/useCanvas.js +126 -0
  465. package/src/internals/canvas/useCanvas.test.js +26 -0
  466. package/src/internals/canvas/useMarqueeSelect.js +213 -0
  467. package/src/internals/canvas/useMarqueeSelect.test.js +78 -0
  468. package/src/internals/canvas/useUndoRedo.js +86 -0
  469. package/src/internals/canvas/useUndoRedo.test.js +231 -0
  470. package/src/internals/canvas/widgets/CodePenEmbed.jsx +293 -0
  471. package/src/internals/canvas/widgets/CodePenEmbed.module.css +161 -0
  472. package/src/internals/canvas/widgets/ComponentSetWidget.jsx +2 -0
  473. package/src/internals/canvas/widgets/ComponentSetWidget.module.css +89 -0
  474. package/src/internals/canvas/widgets/ComponentWidget.jsx +14 -0
  475. package/src/internals/canvas/widgets/ComponentWidget.module.css +0 -0
  476. package/src/internals/canvas/widgets/CropOverlay.jsx +179 -0
  477. package/src/internals/canvas/widgets/CropOverlay.module.css +154 -0
  478. package/src/internals/canvas/widgets/ExpandedPane.jsx +474 -0
  479. package/src/internals/canvas/widgets/ExpandedPane.module.css +179 -0
  480. package/src/internals/canvas/widgets/ExpandedPane.test.jsx +240 -0
  481. package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
  482. package/src/internals/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
  483. package/src/internals/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
  484. package/src/internals/canvas/widgets/FigmaEmbed.jsx +296 -0
  485. package/src/internals/canvas/widgets/FigmaEmbed.module.css +222 -0
  486. package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +151 -0
  487. package/src/internals/canvas/widgets/FrozenTerminalOverlay.module.css +83 -0
  488. package/src/internals/canvas/widgets/ImageWidget.jsx +287 -0
  489. package/src/internals/canvas/widgets/ImageWidget.module.css +81 -0
  490. package/src/internals/canvas/widgets/LinkPreview.jsx +439 -0
  491. package/src/internals/canvas/widgets/LinkPreview.module.css +585 -0
  492. package/src/internals/canvas/widgets/LinkPreview.test.jsx +193 -0
  493. package/src/internals/canvas/widgets/MarkdownBlock.jsx +354 -0
  494. package/src/internals/canvas/widgets/MarkdownBlock.module.css +377 -0
  495. package/src/internals/canvas/widgets/MarkdownBlock.test.jsx +92 -0
  496. package/src/internals/canvas/widgets/PromptWidget.jsx +428 -0
  497. package/src/internals/canvas/widgets/PromptWidget.module.css +273 -0
  498. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +463 -0
  499. package/src/internals/canvas/widgets/PrototypeEmbed.module.css +579 -0
  500. package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +10 -0
  501. package/src/internals/canvas/widgets/ResizeHandle.jsx +67 -0
  502. package/src/internals/canvas/widgets/ResizeHandle.module.css +29 -0
  503. package/src/internals/canvas/widgets/StickyNote.jsx +92 -0
  504. package/src/internals/canvas/widgets/StickyNote.module.css +70 -0
  505. package/src/internals/canvas/widgets/StickyNote.test.jsx +116 -0
  506. package/src/internals/canvas/widgets/StorySetWidget.jsx +208 -0
  507. package/src/internals/canvas/widgets/StorySetWidget.module.css +89 -0
  508. package/src/internals/canvas/widgets/StoryWidget.jsx +334 -0
  509. package/src/internals/canvas/widgets/StoryWidget.module.css +211 -0
  510. package/src/internals/canvas/widgets/TerminalReadWidget.jsx +146 -0
  511. package/src/internals/canvas/widgets/TerminalReadWidget.module.css +94 -0
  512. package/src/internals/canvas/widgets/TerminalWidget.jsx +704 -0
  513. package/src/internals/canvas/widgets/TerminalWidget.module.css +444 -0
  514. package/src/internals/canvas/widgets/TilesWidget.jsx +300 -0
  515. package/src/internals/canvas/widgets/TilesWidget.module.css +133 -0
  516. package/src/internals/canvas/widgets/WidgetChrome.jsx +580 -0
  517. package/src/internals/canvas/widgets/WidgetChrome.module.css +421 -0
  518. package/src/internals/canvas/widgets/WidgetWrapper.jsx +15 -0
  519. package/src/internals/canvas/widgets/WidgetWrapper.module.css +25 -0
  520. package/src/internals/canvas/widgets/codepenUrl.js +75 -0
  521. package/src/internals/canvas/widgets/codepenUrl.test.js +76 -0
  522. package/src/internals/canvas/widgets/embedInteraction.test.jsx +173 -0
  523. package/src/internals/canvas/widgets/embedOverlay.module.css +35 -0
  524. package/src/internals/canvas/widgets/embedTheme.js +148 -0
  525. package/src/internals/canvas/widgets/expandUtils.js +559 -0
  526. package/src/internals/canvas/widgets/expandUtils.test.js +155 -0
  527. package/src/internals/canvas/widgets/figmaUrl.js +118 -0
  528. package/src/internals/canvas/widgets/figmaUrl.test.js +139 -0
  529. package/src/internals/canvas/widgets/githubUrl.js +82 -0
  530. package/src/internals/canvas/widgets/githubUrl.test.js +74 -0
  531. package/src/internals/canvas/widgets/iframeDevLogs.js +49 -0
  532. package/src/internals/canvas/widgets/iframeDevLogs.test.jsx +81 -0
  533. package/src/internals/canvas/widgets/index.js +42 -0
  534. package/src/internals/canvas/widgets/pasteRules.js +295 -0
  535. package/src/internals/canvas/widgets/pasteRules.test.js +474 -0
  536. package/src/internals/canvas/widgets/snapshotDisplay.test.jsx +211 -0
  537. package/src/internals/canvas/widgets/tilePool.js +23 -0
  538. package/src/internals/canvas/widgets/tiles/diagonal-bl.png +0 -0
  539. package/src/internals/canvas/widgets/tiles/diagonal-br.png +0 -0
  540. package/src/internals/canvas/widgets/tiles/diagonal-tl.png +0 -0
  541. package/src/internals/canvas/widgets/tiles/leaf.png +0 -0
  542. package/src/internals/canvas/widgets/tiles/quarter-tl.png +0 -0
  543. package/src/internals/canvas/widgets/tiles/quarter-tr.png +0 -0
  544. package/src/internals/canvas/widgets/tiles/solid-a.png +0 -0
  545. package/src/internals/canvas/widgets/tiles/solid-b.png +0 -0
  546. package/src/internals/canvas/widgets/widgetConfig.js +291 -0
  547. package/src/internals/canvas/widgets/widgetConfig.test.js +68 -0
  548. package/src/internals/canvas/widgets/widgetIcons.jsx +190 -0
  549. package/src/internals/canvas/widgets/widgetProps.js +133 -0
  550. package/src/internals/context/FormContext.js +13 -0
  551. package/src/internals/context/FormContext.test.js +48 -0
  552. package/src/internals/context.jsx +481 -0
  553. package/src/internals/context.test.jsx +296 -0
  554. package/src/internals/hashPreserver.js +73 -0
  555. package/src/internals/hashPreserver.test.js +107 -0
  556. package/src/internals/hooks/useConfig.js +14 -0
  557. package/src/internals/hooks/useFeatureFlag.js +14 -0
  558. package/src/internals/hooks/useFlows.js +50 -0
  559. package/src/internals/hooks/useFlows.test.js +134 -0
  560. package/src/internals/hooks/useHideMode.js +31 -0
  561. package/src/internals/hooks/useHideMode.test.js +43 -0
  562. package/src/internals/hooks/useLocalStorage.js +57 -0
  563. package/src/internals/hooks/useLocalStorage.test.js +75 -0
  564. package/src/internals/hooks/useMode.js +43 -0
  565. package/src/internals/hooks/useObject.js +101 -0
  566. package/src/internals/hooks/useObject.test.js +74 -0
  567. package/src/internals/hooks/useOverride.js +84 -0
  568. package/src/internals/hooks/useOverride.test.js +71 -0
  569. package/src/internals/hooks/usePrototypeReloadGuard.js +64 -0
  570. package/src/internals/hooks/useRecord.js +158 -0
  571. package/src/internals/hooks/useRecord.test.js +221 -0
  572. package/src/internals/hooks/useScene.js +38 -0
  573. package/src/internals/hooks/useScene.test.js +66 -0
  574. package/src/internals/hooks/useSceneData.js +108 -0
  575. package/src/internals/hooks/useSceneData.test.js +136 -0
  576. package/src/internals/hooks/useSession.js +4 -0
  577. package/src/internals/hooks/useSession.test.js +8 -0
  578. package/src/internals/hooks/useThemeState.js +61 -0
  579. package/src/internals/hooks/useThemeState.test.js +66 -0
  580. package/src/internals/hooks/useUndoRedo.js +28 -0
  581. package/src/internals/hooks/useUndoRedo.test.js +64 -0
  582. package/src/internals/index.js +58 -0
  583. package/src/internals/story/ComponentSetPage.jsx +198 -0
  584. package/src/internals/story/ComponentSetPage.module.css +129 -0
  585. package/src/internals/story/StoryPage.jsx +147 -0
  586. package/src/internals/story/StoryPage.module.css +18 -0
  587. package/src/internals/test-utils.js +45 -0
  588. package/src/internals/vite/data-plugin.js +1508 -0
  589. package/src/internals/vite/data-plugin.test.js +1223 -0
  590. package/src/test-utils.js +44 -0
  591. package/toolbar.config.json +271 -0
  592. package/widgets.config.json +1537 -0
@@ -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
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Auth modal — delegates to the React PATDialog via a custom event.
3
+ *
4
+ * The AuthModal is no longer mounted directly. Instead, we dispatch
5
+ * a 'storyboard:open-auth-modal' event that the React ViewfinderNew listens for.
6
+ */
7
+
8
+ import { getCachedUser, clearToken } from '../auth.js'
9
+
10
+ /**
11
+ * Open the auth modal. Dispatches a custom event to trigger the React PATDialog.
12
+ * @param {{ initialError?: string|null }} [options]
13
+ * @returns {Promise<{ login: string, avatarUrl: string }|null>}
14
+ */
15
+ export function openAuthModal(options = {}) {
16
+ document.dispatchEvent(new CustomEvent('storyboard:open-auth-modal', {
17
+ detail: options,
18
+ }))
19
+
20
+ // Return a promise that resolves when token appears in localStorage
21
+ return new Promise((resolve) => {
22
+ function onStorage() {
23
+ try {
24
+ const token = localStorage.getItem('sb-comments-token')
25
+ if (token) {
26
+ window.removeEventListener('storage', onStorage)
27
+ clearInterval(pollId)
28
+ const user = getCachedUser()
29
+ resolve(user || { login: 'user', avatarUrl: '' })
30
+ }
31
+ } catch { /* ignore */ }
32
+ }
33
+
34
+ // Poll localStorage for token changes (same-window writes don't fire 'storage')
35
+ const initialToken = (() => { try { return localStorage.getItem('sb-comments-token') } catch { return null } })()
36
+ const pollId = setInterval(() => {
37
+ try {
38
+ const token = localStorage.getItem('sb-comments-token')
39
+ if (token && token !== initialToken) {
40
+ clearInterval(pollId)
41
+ window.removeEventListener('storage', onStorage)
42
+ const user = getCachedUser()
43
+ resolve(user || { login: 'user', avatarUrl: '' })
44
+ }
45
+ } catch { /* ignore */ }
46
+ }, 500)
47
+
48
+ window.addEventListener('storage', onStorage)
49
+
50
+ // Timeout after 5 minutes
51
+ setTimeout(() => {
52
+ clearInterval(pollId)
53
+ window.removeEventListener('storage', onStorage)
54
+ resolve(null)
55
+ }, 5 * 60 * 1000)
56
+ })
57
+ }
58
+
59
+ /**
60
+ * Open a sign-out confirmation. Clears token immediately.
61
+ */
62
+ export function signOut() {
63
+ const user = getCachedUser()
64
+ clearToken()
65
+ console.log(`[storyboard] Signed out${user ? ` (was ${user.login})` : ''}`)
66
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Tests for authModal.js — PAT entry modal lifecycle.
3
+ *
4
+ * Tests the modal's resolve/reject/close behavior via the imperative API.
5
+ * Runs under the React vitest config since authModal.js imports a React component.
6
+ */
7
+
8
+ import { vi } from 'vitest'
9
+
10
+ describe('authModal.js', () => {
11
+ let openAuthModal, signOut
12
+
13
+ beforeEach(async () => {
14
+ document.body.innerHTML = ''
15
+ localStorage.clear()
16
+
17
+ vi.resetModules()
18
+
19
+ const mod = await import('./authModal.js')
20
+ openAuthModal = mod.openAuthModal
21
+ signOut = mod.signOut
22
+ })
23
+
24
+ afterEach(() => {
25
+ vi.restoreAllMocks()
26
+ })
27
+
28
+ it('creates a backdrop element in the DOM', () => {
29
+ openAuthModal()
30
+
31
+ const backdrop = document.getElementById('sb-auth-modal')
32
+ expect(backdrop).not.toBeNull()
33
+ expect(backdrop.classList.contains('sb-auth-backdrop')).toBe(true)
34
+ })
35
+
36
+ it('removes old modal before creating a new one', () => {
37
+ openAuthModal()
38
+ openAuthModal()
39
+
40
+ const modals = document.querySelectorAll('#sb-auth-modal')
41
+ expect(modals.length).toBe(1)
42
+ })
43
+
44
+ it('resolves null when Escape is pressed', async () => {
45
+ const promise = openAuthModal()
46
+
47
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
48
+
49
+ const result = await promise
50
+ expect(result).toBeNull()
51
+ expect(document.getElementById('sb-auth-modal')).toBeNull()
52
+ })
53
+
54
+ it('resolves null when backdrop is clicked', async () => {
55
+ const promise = openAuthModal()
56
+
57
+ const backdrop = document.getElementById('sb-auth-modal')
58
+ backdrop.dispatchEvent(new MouseEvent('click', { bubbles: true }))
59
+
60
+ const result = await promise
61
+ expect(result).toBeNull()
62
+ expect(document.getElementById('sb-auth-modal')).toBeNull()
63
+ })
64
+
65
+ describe('signOut', () => {
66
+ it('clears the stored token', () => {
67
+ localStorage.setItem('sb-comments-token', 'ghp_test')
68
+ localStorage.setItem('sb-comments-user', JSON.stringify({ login: 'test' }))
69
+
70
+ signOut()
71
+
72
+ expect(localStorage.getItem('sb-comments-token')).toBeNull()
73
+ expect(localStorage.getItem('sb-comments-user')).toBeNull()
74
+ })
75
+ })
76
+ })
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#1c2128" stroke="#fff" stroke-width="1.5" d="M19.503 9.97c1.204.489 1.112 2.224-.137 2.583l-6.305 1.813-2.88 5.895c-.571 1.168-2.296.957-2.569-.314L4.677 6.257A1.369 1.369 0 0 1 6.53 4.7z" clip-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#fff" stroke="#1f2328" stroke-width="1.5" d="M19.503 9.97c1.204.489 1.112 2.224-.137 2.583l-6.305 1.813-2.88 5.895c-.571 1.168-2.296.957-2.569-.314L4.677 6.257A1.369 1.369 0 0 1 6.53 4.7z" clip-rule="evenodd"/></svg>