@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,151 @@
1
+ /**
2
+ * PAT authentication for comments.
3
+ *
4
+ * Stores and retrieves the GitHub PAT from localStorage.
5
+ * Provides validation by fetching the authenticated user and
6
+ * verifying the token can access repository discussions.
7
+ */
8
+
9
+ import { getCommentsConfig } from './config.js'
10
+
11
+ const STORAGE_KEY = 'sb-comments-token'
12
+ const USER_KEY = 'sb-comments-user'
13
+ const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql'
14
+
15
+ /**
16
+ * Get the stored PAT token.
17
+ * @returns {string|null}
18
+ */
19
+ export function getToken() {
20
+ try {
21
+ return localStorage.getItem(STORAGE_KEY)
22
+ } catch {
23
+ return null
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Store a PAT token.
29
+ * @param {string} token
30
+ */
31
+ export function setToken(token) {
32
+ localStorage.setItem(STORAGE_KEY, token)
33
+ }
34
+
35
+ /**
36
+ * Remove the stored PAT token and user.
37
+ */
38
+ export function clearToken() {
39
+ localStorage.removeItem(STORAGE_KEY)
40
+ localStorage.removeItem(USER_KEY)
41
+ }
42
+
43
+ /**
44
+ * Get the cached authenticated user info.
45
+ * @returns {{ login: string, avatarUrl: string }|null}
46
+ */
47
+ export function getCachedUser() {
48
+ try {
49
+ const raw = localStorage.getItem(USER_KEY)
50
+ return raw ? JSON.parse(raw) : null
51
+ } catch {
52
+ return null
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Validate a PAT by fetching the authenticated user from GitHub,
58
+ * then probing the GraphQL API to verify the token can access
59
+ * the configured repository's discussions.
60
+ *
61
+ * Caches the user in localStorage on success.
62
+ * @param {string} token - GitHub PAT to validate
63
+ * @returns {Promise<{ login: string, avatarUrl: string }>}
64
+ */
65
+ export async function validateToken(token) {
66
+ // 1. Verify token is a valid GitHub token
67
+ const res = await fetch('https://api.github.com/user', {
68
+ headers: { Authorization: `bearer ${token}` },
69
+ })
70
+
71
+ if (!res.ok) {
72
+ throw new Error('Invalid token — GitHub returned ' + res.status)
73
+ }
74
+
75
+ const user = await res.json()
76
+ const scopes = (res.headers.get('x-oauth-scopes') || '').split(',').map(s => s.trim()).filter(Boolean)
77
+ const userInfo = { login: user.login, avatarUrl: user.avatar_url, scopes }
78
+
79
+ // 2. Verify the token can access repository discussions
80
+ await validateTokenPermissions(token)
81
+
82
+ localStorage.setItem(USER_KEY, JSON.stringify(userInfo))
83
+ return userInfo
84
+ }
85
+
86
+ /**
87
+ * Probe the GraphQL API to verify the token has access to the
88
+ * configured repository's discussions. Throws a descriptive error
89
+ * if the token lacks the required scopes.
90
+ * @param {string} token - GitHub PAT to test
91
+ */
92
+ async function validateTokenPermissions(token) {
93
+ const config = getCommentsConfig()
94
+ if (!config) return // no config = nothing to probe
95
+
96
+ const { owner, name } = config.repo
97
+ if (!owner || !name) return
98
+
99
+ const query = `query { repository(owner: "${owner}", name: "${name}") { id discussionCategories(first: 1) { nodes { id } } } }`
100
+
101
+ const res = await fetch(GITHUB_GRAPHQL_URL, {
102
+ method: 'POST',
103
+ headers: {
104
+ Authorization: `bearer ${token}`,
105
+ 'Content-Type': 'application/json',
106
+ },
107
+ body: JSON.stringify({ query }),
108
+ })
109
+
110
+ if (res.status === 401) {
111
+ throw new Error('Token is invalid or expired.')
112
+ }
113
+
114
+ if (!res.ok) {
115
+ throw new Error(`GitHub API error: ${res.status}`)
116
+ }
117
+
118
+ const json = await res.json()
119
+
120
+ if (json.errors?.length) {
121
+ const msg = json.errors.map((e) => e.message).join(', ')
122
+ if (msg.includes('not accessible') || msg.includes('insufficient')) {
123
+ throw new Error(
124
+ `Token doesn't have access to ${owner}/${name} discussions. ` +
125
+ 'Fine-grained tokens need "Discussions: Read and write". ' +
126
+ 'Classic tokens need the "repo" scope.'
127
+ )
128
+ }
129
+ throw new Error(`GitHub API error: ${msg}`)
130
+ }
131
+
132
+ if (!json.data?.repository) {
133
+ throw new Error(
134
+ `Repository ${owner}/${name} not found. Check that the token has access to this repository.`
135
+ )
136
+ }
137
+
138
+ if (!json.data.repository.discussionCategories?.nodes?.length) {
139
+ throw new Error(
140
+ `No discussion categories found in ${owner}/${name}. Enable Discussions in the repository settings.`
141
+ )
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Check whether the user is currently authenticated.
147
+ * @returns {boolean}
148
+ */
149
+ export function isAuthenticated() {
150
+ return getToken() !== null
151
+ }
@@ -0,0 +1,167 @@
1
+ import { getToken, setToken, clearToken, getCachedUser, isAuthenticated, validateToken } from './auth.js'
2
+ import { initCommentsConfig } from './config.js'
3
+
4
+ describe('auth token management', () => {
5
+ beforeEach(() => {
6
+ localStorage.clear()
7
+ })
8
+
9
+ it('returns null when no token is stored', () => {
10
+ expect(getToken()).toBeNull()
11
+ })
12
+
13
+ it('stores and retrieves a token', () => {
14
+ setToken('ghp_test123')
15
+ expect(getToken()).toBe('ghp_test123')
16
+ })
17
+
18
+ it('clears token and user', () => {
19
+ setToken('ghp_test123')
20
+ localStorage.setItem('sb-comments-user', JSON.stringify({ login: 'test' }))
21
+ clearToken()
22
+ expect(getToken()).toBeNull()
23
+ expect(getCachedUser()).toBeNull()
24
+ })
25
+ })
26
+
27
+ describe('getCachedUser', () => {
28
+ beforeEach(() => {
29
+ localStorage.clear()
30
+ })
31
+
32
+ it('returns null when no user is cached', () => {
33
+ expect(getCachedUser()).toBeNull()
34
+ })
35
+
36
+ it('returns cached user info', () => {
37
+ const user = { login: 'dfosco', avatarUrl: 'https://example.com/avatar.png' }
38
+ localStorage.setItem('sb-comments-user', JSON.stringify(user))
39
+ expect(getCachedUser()).toEqual(user)
40
+ })
41
+
42
+ it('returns null on invalid JSON', () => {
43
+ localStorage.setItem('sb-comments-user', 'not json')
44
+ expect(getCachedUser()).toBeNull()
45
+ })
46
+ })
47
+
48
+ describe('isAuthenticated', () => {
49
+ beforeEach(() => {
50
+ localStorage.clear()
51
+ })
52
+
53
+ it('returns false when no token', () => {
54
+ expect(isAuthenticated()).toBe(false)
55
+ })
56
+
57
+ it('returns true when token exists', () => {
58
+ setToken('ghp_abc')
59
+ expect(isAuthenticated()).toBe(true)
60
+ })
61
+ })
62
+
63
+ describe('validateToken', () => {
64
+ beforeEach(() => {
65
+ localStorage.clear()
66
+ globalThis.fetch = vi.fn()
67
+ })
68
+
69
+ afterEach(() => {
70
+ vi.restoreAllMocks()
71
+ })
72
+
73
+ it('throws on invalid token (REST 401)', async () => {
74
+ globalThis.fetch.mockResolvedValueOnce({ ok: false, status: 401 })
75
+ await expect(validateToken('bad_token')).rejects.toThrow('Invalid token')
76
+ })
77
+
78
+ it('validates user and permissions when config is set', async () => {
79
+ initCommentsConfig({
80
+ comments: { discussions: { category: 'Comments' } },
81
+ repository: { owner: 'testorg', name: 'testrepo' },
82
+ })
83
+
84
+ // REST /user succeeds
85
+ globalThis.fetch.mockResolvedValueOnce({
86
+ ok: true,
87
+ json: () => Promise.resolve({ login: 'testuser', avatar_url: 'https://img/avatar' }),
88
+ })
89
+ // GraphQL permissions probe succeeds
90
+ globalThis.fetch.mockResolvedValueOnce({
91
+ ok: true,
92
+ status: 200,
93
+ json: () => Promise.resolve({
94
+ data: {
95
+ repository: {
96
+ id: 'R_123',
97
+ discussionCategories: { nodes: [{ id: 'DC_1' }] },
98
+ },
99
+ },
100
+ }),
101
+ })
102
+
103
+ const user = await validateToken('ghp_valid')
104
+ expect(user).toEqual({ login: 'testuser', avatarUrl: 'https://img/avatar' })
105
+ expect(getCachedUser()).toEqual(user)
106
+ })
107
+
108
+ it('throws descriptive error when token lacks discussion access', async () => {
109
+ initCommentsConfig({
110
+ comments: { discussions: { category: 'Comments' } },
111
+ repository: { owner: 'testorg', name: 'testrepo' },
112
+ })
113
+
114
+ // REST /user succeeds
115
+ globalThis.fetch.mockResolvedValueOnce({
116
+ ok: true,
117
+ json: () => Promise.resolve({ login: 'testuser', avatar_url: 'https://img/avatar' }),
118
+ })
119
+ // GraphQL probe fails with permissions error
120
+ globalThis.fetch.mockResolvedValueOnce({
121
+ ok: true,
122
+ status: 200,
123
+ json: () => Promise.resolve({
124
+ errors: [{ message: 'Resource not accessible by personal access token' }],
125
+ }),
126
+ })
127
+
128
+ await expect(validateToken('ghp_no_scope')).rejects.toThrow(
129
+ /doesn't have access.*discussions/i
130
+ )
131
+ // User should NOT be cached on permission failure
132
+ expect(getCachedUser()).toBeNull()
133
+ })
134
+
135
+ it('throws when repository not found', async () => {
136
+ initCommentsConfig({
137
+ comments: { discussions: { category: 'Comments' } },
138
+ repository: { owner: 'testorg', name: 'missing' },
139
+ })
140
+
141
+ globalThis.fetch.mockResolvedValueOnce({
142
+ ok: true,
143
+ json: () => Promise.resolve({ login: 'testuser', avatar_url: 'https://img/avatar' }),
144
+ })
145
+ globalThis.fetch.mockResolvedValueOnce({
146
+ ok: true,
147
+ status: 200,
148
+ json: () => Promise.resolve({ data: { repository: null } }),
149
+ })
150
+
151
+ await expect(validateToken('ghp_valid')).rejects.toThrow(/not found/)
152
+ })
153
+
154
+ it('skips permission check when no comments config', async () => {
155
+ initCommentsConfig({}) // no comments key
156
+
157
+ globalThis.fetch.mockResolvedValueOnce({
158
+ ok: true,
159
+ json: () => Promise.resolve({ login: 'testuser', avatar_url: 'https://img/avatar' }),
160
+ })
161
+
162
+ const user = await validateToken('ghp_valid')
163
+ expect(user.login).toBe('testuser')
164
+ // Only 1 fetch call (REST), no GraphQL probe
165
+ expect(globalThis.fetch).toHaveBeenCalledTimes(1)
166
+ })
167
+ })
@@ -0,0 +1,109 @@
1
+ /**
2
+ * localStorage cache for comment pin data.
3
+ *
4
+ * Stores lightweight comment listings per route so pins render instantly
5
+ * on repeat visits without hitting the GitHub API.
6
+ */
7
+
8
+ const CACHE_PREFIX = 'sb-comments:'
9
+ const TTL_MS = 2 * 60 * 1000 // 2 minutes
10
+
11
+ /**
12
+ * Get cached comment listing for a route.
13
+ * Returns null if cache is missing or expired.
14
+ * @param {string} route
15
+ * @returns {object|null} - Cached discussion object (with .comments array)
16
+ */
17
+ export function getCachedComments(route) {
18
+ try {
19
+ const raw = localStorage.getItem(CACHE_PREFIX + route)
20
+ if (!raw) return null
21
+ const entry = JSON.parse(raw)
22
+ if (Date.now() - entry.ts > TTL_MS) {
23
+ localStorage.removeItem(CACHE_PREFIX + route)
24
+ return null
25
+ }
26
+ return entry.data
27
+ } catch {
28
+ return null
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Store comment listing in cache for a route.
34
+ * @param {string} route
35
+ * @param {object} data - Discussion object with .comments array
36
+ */
37
+ export function setCachedComments(route, data) {
38
+ try {
39
+ localStorage.setItem(CACHE_PREFIX + route, JSON.stringify({ ts: Date.now(), data }))
40
+ } catch {
41
+ // localStorage full or unavailable — ignore
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Clear cached comments for a specific route.
47
+ * @param {string} route
48
+ */
49
+ export function clearCachedComments(route) {
50
+ try {
51
+ localStorage.removeItem(CACHE_PREFIX + route)
52
+ } catch {
53
+ // ignore
54
+ }
55
+ }
56
+
57
+ // --- Pending (failed) comments ---
58
+
59
+ const PENDING_PREFIX = 'sb-pending-comments:'
60
+
61
+ /**
62
+ * Save a pending comment that failed to submit.
63
+ * @param {string} route
64
+ * @param {{ id: string, x: number, y: number, text: string, author: object }} comment
65
+ */
66
+ export function savePendingComment(route, comment) {
67
+ try {
68
+ const pending = getPendingComments(route)
69
+ // Replace if same id already exists, else append
70
+ const idx = pending.findIndex(c => c.id === comment.id)
71
+ if (idx >= 0) pending[idx] = comment
72
+ else pending.push(comment)
73
+ localStorage.setItem(PENDING_PREFIX + route, JSON.stringify(pending))
74
+ } catch {
75
+ // ignore
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Get all pending (failed) comments for a route.
81
+ * @param {string} route
82
+ * @returns {Array<{ id: string, x: number, y: number, text: string, author: object }>}
83
+ */
84
+ export function getPendingComments(route) {
85
+ try {
86
+ const raw = localStorage.getItem(PENDING_PREFIX + route)
87
+ return raw ? JSON.parse(raw) : []
88
+ } catch {
89
+ return []
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Remove a pending comment (after successful retry or dismissal).
95
+ * @param {string} route
96
+ * @param {string} pendingId
97
+ */
98
+ export function removePendingComment(route, pendingId) {
99
+ try {
100
+ const pending = getPendingComments(route).filter(c => c.id !== pendingId)
101
+ if (pending.length > 0) {
102
+ localStorage.setItem(PENDING_PREFIX + route, JSON.stringify(pending))
103
+ } else {
104
+ localStorage.removeItem(PENDING_PREFIX + route)
105
+ }
106
+ } catch {
107
+ // ignore
108
+ }
109
+ }
@@ -0,0 +1,48 @@
1
+ import { getCachedComments, setCachedComments, clearCachedComments } from './commentCache.js'
2
+
3
+ describe('commentCache', () => {
4
+ beforeEach(() => {
5
+ localStorage.clear()
6
+ })
7
+
8
+ it('returns null for uncached route', () => {
9
+ expect(getCachedComments('/Overview')).toBeNull()
10
+ })
11
+
12
+ it('stores and retrieves cached comments', () => {
13
+ const data = { id: 'D_1', comments: [{ id: 'C_1', meta: { x: 10, y: 20 } }] }
14
+ setCachedComments('/Overview', data)
15
+ const cached = getCachedComments('/Overview')
16
+ expect(cached).toEqual(data)
17
+ })
18
+
19
+ it('returns null for expired cache', () => {
20
+ const data = { id: 'D_1', comments: [] }
21
+ // Manually write expired entry
22
+ localStorage.setItem('sb-comments:/Overview', JSON.stringify({
23
+ ts: Date.now() - 3 * 60 * 1000, // 3 min ago (exceeds 2-min TTL)
24
+ data,
25
+ }))
26
+ expect(getCachedComments('/Overview')).toBeNull()
27
+ })
28
+
29
+ it('clears cached comments for a route', () => {
30
+ setCachedComments('/Overview', { id: 'D_1', comments: [] })
31
+ clearCachedComments('/Overview')
32
+ expect(getCachedComments('/Overview')).toBeNull()
33
+ })
34
+
35
+ it('does not affect other routes when clearing', () => {
36
+ const data1 = { id: 'D_1', comments: [] }
37
+ const data2 = { id: 'D_2', comments: [] }
38
+ setCachedComments('/Overview', data1)
39
+ setCachedComments('/Issues', data2)
40
+ clearCachedComments('/Overview')
41
+ expect(getCachedComments('/Issues')).toEqual(data2)
42
+ })
43
+
44
+ it('handles corrupted localStorage gracefully', () => {
45
+ localStorage.setItem('sb-comments:/Overview', 'not-json')
46
+ expect(getCachedComments('/Overview')).toBeNull()
47
+ })
48
+ })
@@ -0,0 +1,68 @@
1
+ /**
2
+ * localStorage persistence for comment drafts.
3
+ *
4
+ * Saves in-progress comment text so it survives window close/reopen.
5
+ * Each draft is a structured entry with a type discriminator:
6
+ * - { type: 'comment', text } — top-level comment (keyed by route)
7
+ * - { type: 'reply', text } — reply to an existing thread (keyed by comment ID)
8
+ */
9
+
10
+ const STORAGE_KEY = 'sb-comment-drafts'
11
+
12
+ /** @returns {Record<string, { type: string, text: string }>} */
13
+ function readDrafts() {
14
+ try {
15
+ return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}')
16
+ } catch {
17
+ return {}
18
+ }
19
+ }
20
+
21
+ /** @param {Record<string, { type: string, text: string }>} drafts */
22
+ function writeDrafts(drafts) {
23
+ try {
24
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts))
25
+ } catch {
26
+ // localStorage full or unavailable
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Save a draft entry.
32
+ * @param {string} key - Storage key (use composerDraftKey / replyDraftKey)
33
+ * @param {{ type: string, text: string }} draft
34
+ */
35
+ export function saveDraft(key, draft) {
36
+ const drafts = readDrafts()
37
+ drafts[key] = draft
38
+ writeDrafts(drafts)
39
+ }
40
+
41
+ /**
42
+ * Get a draft entry by key. Returns null if not found.
43
+ * @param {string} key
44
+ * @returns {{ type: string, text: string } | null}
45
+ */
46
+ export function getDraft(key) {
47
+ return readDrafts()[key] ?? null
48
+ }
49
+
50
+ /**
51
+ * Clear a draft entry.
52
+ * @param {string} key
53
+ */
54
+ export function clearDraft(key) {
55
+ const drafts = readDrafts()
56
+ delete drafts[key]
57
+ writeDrafts(drafts)
58
+ }
59
+
60
+ /** Key for a top-level comment draft on a given route. */
61
+ export function composerDraftKey(route) {
62
+ return `comment:${route}`
63
+ }
64
+
65
+ /** Key for a reply draft on a given comment thread. */
66
+ export function replyDraftKey(commentId) {
67
+ return `reply:${commentId}`
68
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Comment mode state — manages the toggle between normal and comment mode.
3
+ *
4
+ * When active: cursor changes to crosshair, comment pins are visible,
5
+ * clicking places a new comment.
6
+ */
7
+
8
+ import { isCommentsEnabled } from './config.js'
9
+ import { isAuthenticated } from './auth.js'
10
+
11
+ let _active = false
12
+ const _listeners = new Set()
13
+
14
+ /**
15
+ * Check whether comment mode is currently active.
16
+ */
17
+ export function isCommentModeActive() {
18
+ return _active
19
+ }
20
+
21
+ /**
22
+ * Toggle comment mode on/off.
23
+ * Only activates if comments are enabled and user is authenticated.
24
+ * @returns {boolean} The new state
25
+ */
26
+ export function toggleCommentMode() {
27
+ if (!isCommentsEnabled()) {
28
+ console.warn('[storyboard] Comments not enabled — check storyboard.config.json')
29
+ return false
30
+ }
31
+
32
+ if (!_active && !isAuthenticated()) {
33
+ console.warn('[storyboard] Sign in first to use comments')
34
+ return false
35
+ }
36
+
37
+ _active = !_active
38
+ _notify()
39
+ return _active
40
+ }
41
+
42
+ /**
43
+ * Explicitly set comment mode.
44
+ * @param {boolean} active
45
+ */
46
+ export function setCommentMode(active) {
47
+ _active = active
48
+ _notify()
49
+ }
50
+
51
+ /**
52
+ * Subscribe to comment mode changes.
53
+ * @param {(active: boolean) => void} callback
54
+ * @returns {() => void} Unsubscribe function
55
+ */
56
+ export function subscribeToCommentMode(callback) {
57
+ _listeners.add(callback)
58
+ return () => _listeners.delete(callback)
59
+ }
60
+
61
+ function _notify() {
62
+ for (const cb of _listeners) cb(_active)
63
+ }
@@ -0,0 +1,90 @@
1
+ import {
2
+ isCommentModeActive,
3
+ toggleCommentMode,
4
+ setCommentMode,
5
+ subscribeToCommentMode,
6
+ } from './commentMode.js'
7
+ import { initCommentsConfig } from './config.js'
8
+ import { setToken, clearToken } from './auth.js'
9
+
10
+ describe('commentMode', () => {
11
+ beforeEach(() => {
12
+ // Reset state
13
+ setCommentMode(false)
14
+ clearToken()
15
+ initCommentsConfig(null)
16
+ })
17
+
18
+ it('starts inactive', () => {
19
+ setCommentMode(false)
20
+ expect(isCommentModeActive()).toBe(false)
21
+ })
22
+
23
+ it('setCommentMode activates and deactivates', () => {
24
+ setCommentMode(true)
25
+ expect(isCommentModeActive()).toBe(true)
26
+ setCommentMode(false)
27
+ expect(isCommentModeActive()).toBe(false)
28
+ })
29
+
30
+ it('toggleCommentMode returns false when comments not enabled', () => {
31
+ const result = toggleCommentMode()
32
+ expect(result).toBe(false)
33
+ expect(isCommentModeActive()).toBe(false)
34
+ })
35
+
36
+ it('toggleCommentMode returns false when not authenticated', () => {
37
+ initCommentsConfig({
38
+ comments: { discussions: { category: 'Test' } },
39
+ repository: { owner: 'o', name: 'r' },
40
+ })
41
+ const result = toggleCommentMode()
42
+ expect(result).toBe(false)
43
+ expect(isCommentModeActive()).toBe(false)
44
+ })
45
+
46
+ it('toggleCommentMode activates when enabled and authenticated', () => {
47
+ initCommentsConfig({
48
+ comments: { discussions: { category: 'Test' } },
49
+ repository: { owner: 'o', name: 'r' },
50
+ })
51
+ setToken('ghp_test')
52
+ const result = toggleCommentMode()
53
+ expect(result).toBe(true)
54
+ expect(isCommentModeActive()).toBe(true)
55
+ })
56
+
57
+ it('toggleCommentMode toggles off when active', () => {
58
+ initCommentsConfig({
59
+ comments: { discussions: { category: 'Test' } },
60
+ repository: { owner: 'o', name: 'r' },
61
+ })
62
+ setToken('ghp_test')
63
+ toggleCommentMode() // on
64
+ const result = toggleCommentMode() // off
65
+ expect(result).toBe(false)
66
+ expect(isCommentModeActive()).toBe(false)
67
+ })
68
+
69
+ it('subscribeToCommentMode calls callback on changes', () => {
70
+ const calls = []
71
+ subscribeToCommentMode((active) => calls.push(active))
72
+
73
+ setCommentMode(true)
74
+ setCommentMode(false)
75
+ setCommentMode(true)
76
+
77
+ expect(calls).toEqual([true, false, true])
78
+ })
79
+
80
+ it('subscribeToCommentMode returns unsubscribe function', () => {
81
+ const calls = []
82
+ const unsub = subscribeToCommentMode((active) => calls.push(active))
83
+
84
+ setCommentMode(true)
85
+ unsub()
86
+ setCommentMode(false)
87
+
88
+ expect(calls).toEqual([true])
89
+ })
90
+ })