@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,1479 @@
1
+ /**
2
+ * Workspace — SaaS-style homescreen for Storyboard.
3
+ *
4
+ * Sidebar + grid layout wired to real data from buildPrototypeIndex and listStories.
5
+ * Formerly known as Viewfinder — renamed to match the /workspace route.
6
+ */
7
+ import { useState, useEffect, useRef, useMemo, useCallback, useSyncExternalStore } from 'react'
8
+ import { buildPrototypeIndex, listStories, getStoryData, BranchSelect } from '../core/index.js'
9
+ import { MarkGithubIcon, GitBranchIcon, ChevronDownIcon, ChevronRightIcon, PlusIcon, StarIcon, StarFillIcon, ThreeBarsIcon, XIcon, StackIcon, TrashIcon, ShieldLockIcon, KebabHorizontalIcon, PencilIcon } from '@primer/octicons-react'
10
+ import { Menu } from '@base-ui/react/menu'
11
+ import { Dialog } from '@base-ui/react/dialog'
12
+ import Icon from './Icon.jsx'
13
+ import { useBranches } from './BranchBar/useBranches.js'
14
+ import css from './Viewfinder.module.css'
15
+
16
+ /* ─── Theme sync: read toolbar theme from DOM and apply to Primer/BaseUI ─── */
17
+
18
+ function getToolbarThemeAttrs() {
19
+ const theme = document.documentElement.getAttribute('data-sb-toolbar-theme') || 'light'
20
+ if (theme === 'dark_dimmed') {
21
+ return { 'data-color-mode': 'dark', 'data-dark-theme': 'dark_dimmed', 'data-light-theme': 'light' }
22
+ }
23
+ if (theme.startsWith('dark')) {
24
+ return { 'data-color-mode': 'dark', 'data-dark-theme': 'dark', 'data-light-theme': 'light' }
25
+ }
26
+ return { 'data-color-mode': 'light', 'data-light-theme': 'light', 'data-dark-theme': 'dark' }
27
+ }
28
+
29
+ function useToolbarTheme() {
30
+ const [attrs, setAttrs] = useState(getToolbarThemeAttrs)
31
+
32
+ useEffect(() => {
33
+ const update = () => setAttrs(getToolbarThemeAttrs())
34
+ const observer = new MutationObserver(update)
35
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-sb-toolbar-theme'] })
36
+ update()
37
+ return () => observer.disconnect()
38
+ }, [])
39
+
40
+ // Sync to document.body so BaseUI portals inherit Primer theme
41
+ useEffect(() => {
42
+ for (const [key, value] of Object.entries(attrs)) {
43
+ document.body.setAttribute(key, value)
44
+ }
45
+ }, [attrs])
46
+
47
+ return attrs
48
+ }
49
+
50
+ /* ─── GitHub user hook ─── */
51
+
52
+ const COMMENTS_USER_KEY = 'sb-comments-user'
53
+ const COMMENTS_TOKEN_KEY = 'sb-comments-token'
54
+
55
+ /**
56
+ * Resolve the current GitHub user for display in the sidebar.
57
+ * Priority: 1) PAT-cached user (from comments auth), 2) gh CLI login via git-user endpoint.
58
+ */
59
+ function useGitHubUser() {
60
+ const [user, setUser] = useState(() => {
61
+ try {
62
+ const raw = localStorage.getItem(COMMENTS_USER_KEY)
63
+ const token = localStorage.getItem(COMMENTS_TOKEN_KEY)
64
+ if (token && raw) {
65
+ const parsed = JSON.parse(raw)
66
+ if (parsed?.login) return parsed
67
+ }
68
+ } catch { /* ignore */ }
69
+ return null
70
+ })
71
+
72
+ // Listen for auth changes (when user signs in via AuthModal)
73
+ useEffect(() => {
74
+ const handler = () => {
75
+ try {
76
+ const raw = localStorage.getItem(COMMENTS_USER_KEY)
77
+ const token = localStorage.getItem(COMMENTS_TOKEN_KEY)
78
+ if (token && raw) {
79
+ const parsed = JSON.parse(raw)
80
+ if (parsed?.login) { setUser(parsed); return }
81
+ }
82
+ setUser(null)
83
+ } catch { setUser(null) }
84
+ }
85
+ window.addEventListener('storage', handler)
86
+ document.addEventListener('storyboard:auth-changed', handler)
87
+ return () => {
88
+ window.removeEventListener('storage', handler)
89
+ document.removeEventListener('storyboard:auth-changed', handler)
90
+ }
91
+ }, [])
92
+
93
+ return user
94
+ }
95
+
96
+ /* ─── localStorage helpers ─── */
97
+
98
+ const STARRED_KEY = 'sb-workspace-starred'
99
+ const RECENT_KEY = 'sb-workspace-recent'
100
+ const MAX_RECENT = 30
101
+ const GROUP_BY_FOLDERS_KEY = 'sb-workspace-group-folders'
102
+
103
+ function readJSON(key, fallback) {
104
+ try { return JSON.parse(localStorage.getItem(key)) || fallback }
105
+ catch { return fallback }
106
+ }
107
+
108
+ function writeJSON(key, value) {
109
+ localStorage.setItem(key, JSON.stringify(value))
110
+ window.dispatchEvent(new StorageEvent('storage', { key }))
111
+ }
112
+
113
+ function createLocalStorageStore(key, fallback) {
114
+ const subscribe = (cb) => {
115
+ const handler = (e) => { if (!e.key || e.key === key) cb() }
116
+ window.addEventListener('storage', handler)
117
+ return () => window.removeEventListener('storage', handler)
118
+ }
119
+ const getSnapshot = () => localStorage.getItem(key) || JSON.stringify(fallback)
120
+ return { subscribe, getSnapshot }
121
+ }
122
+
123
+ const starredStore = createLocalStorageStore(STARRED_KEY, [])
124
+ const recentStore = createLocalStorageStore(RECENT_KEY, [])
125
+
126
+ function useStarred() {
127
+ const raw = useSyncExternalStore(starredStore.subscribe, starredStore.getSnapshot)
128
+ const ids = JSON.parse(raw)
129
+ const toggle = useCallback((id) => {
130
+ const current = readJSON(STARRED_KEY, [])
131
+ const next = current.includes(id) ? current.filter(x => x !== id) : [...current, id]
132
+ writeJSON(STARRED_KEY, next)
133
+ }, [])
134
+ return { starred: new Set(ids), toggle }
135
+ }
136
+
137
+ function useRecent() {
138
+ const raw = useSyncExternalStore(recentStore.subscribe, recentStore.getSnapshot)
139
+ return JSON.parse(raw)
140
+ }
141
+
142
+ function trackRecent(id) {
143
+ const current = readJSON(RECENT_KEY, [])
144
+ const next = [id, ...current.filter(x => x !== id)].slice(0, MAX_RECENT)
145
+ writeJSON(RECENT_KEY, next)
146
+ }
147
+
148
+ /* ─── URL helpers ─── */
149
+
150
+ function withBase(basePath, route) {
151
+ const normalizedRoute = route.startsWith('/') ? route : `/${route}`
152
+ const normalizedBase = (basePath || '/').replace(/\/+$/, '')
153
+ if (!normalizedBase || normalizedBase === '/') return normalizedRoute
154
+ return `${normalizedBase}${normalizedRoute}`.replace(/\/+/g, '/')
155
+ }
156
+
157
+ /* ─── Type helpers ─── */
158
+
159
+ function getTypeLabel(type) {
160
+ if (type === 'prototype') return 'PROTOTYPE'
161
+ if (type === 'canvas') return 'CANVAS'
162
+ if (type === 'component') return 'COMPONENT'
163
+ return type?.toUpperCase() || ''
164
+ }
165
+
166
+ function getTypeIcon(type, size = 14) {
167
+ if (type === 'prototype') return <Icon name="prototype" size={size} />
168
+ if (type === 'canvas') return <Icon name="canvas" size={size} />
169
+ if (type === 'component') return <Icon name="iconoir/keyframe" size={size} />
170
+ return null
171
+ }
172
+
173
+ /* ─── Avatar Stack ─── */
174
+
175
+ function AvatarStack({ authors }) {
176
+ if (!authors || authors.length === 0) return null
177
+ const list = Array.isArray(authors) ? authors : [authors]
178
+ return (
179
+ <div className={css.avatarStack}>
180
+ {list.map(username => (
181
+ <img
182
+ key={username}
183
+ className={css.avatarImg}
184
+ src={`https://github.com/${username}.png?size=48`}
185
+ alt={username}
186
+ width={24}
187
+ height={24}
188
+ loading="lazy"
189
+ onError={(e) => { e.target.style.display = 'none' }}
190
+ />
191
+ ))}
192
+ </div>
193
+ )
194
+ }
195
+
196
+ /* ─── Star Button ─── */
197
+
198
+ function StarBtn({ active, onClick, inline }) {
199
+ const cls = inline
200
+ ? (active ? css.iconBtnInlineActive : css.iconBtnInline)
201
+ : (active ? css.iconBtnActive : css.iconBtn)
202
+ return (
203
+ <button
204
+ className={cls}
205
+ onClick={(e) => { e.preventDefault(); e.stopPropagation(); onClick() }}
206
+ aria-label={active ? 'Remove favorite' : 'Favorite'}
207
+ title={active ? 'Remove favorite' : 'Favorite'}
208
+ >
209
+ {active ? <StarFillIcon size={16} /> : <StarIcon size={16} />}
210
+ </button>
211
+ )
212
+ }
213
+
214
+ /* ─── Card Actions Menu ─── */
215
+
216
+ function CardActionsMenu({ typeLabel, onEdit, onDelete }) {
217
+ return (
218
+ <Menu.Root>
219
+ <Menu.Trigger
220
+ className={css.iconBtn}
221
+ onClick={(e) => { e.preventDefault(); e.stopPropagation() }}
222
+ aria-label="Actions"
223
+ render={<button />}
224
+ >
225
+ <KebabHorizontalIcon size={16} />
226
+ </Menu.Trigger>
227
+ <Menu.Portal>
228
+ <Menu.Positioner className={css.actionsMenuPositioner} side="inline-end" alignment="end" sideOffset={8}>
229
+ <Menu.Popup className={css.actionsMenu} onClick={(e) => { e.preventDefault(); e.stopPropagation() }}>
230
+ <Menu.Item
231
+ className={css.actionsMenuItem}
232
+ onClick={(e) => { e.preventDefault(); e.stopPropagation(); onEdit() }}
233
+ render={<button />}
234
+ >
235
+ <PencilIcon size={16} />
236
+ Edit {typeLabel}
237
+ </Menu.Item>
238
+ <Menu.Item
239
+ className={css.actionsMenuItemDanger}
240
+ onClick={(e) => { e.preventDefault(); e.stopPropagation(); onDelete() }}
241
+ render={<button />}
242
+ >
243
+ <TrashIcon size={16} />
244
+ Delete {typeLabel}
245
+ </Menu.Item>
246
+ </Menu.Popup>
247
+ </Menu.Positioner>
248
+ </Menu.Portal>
249
+ </Menu.Root>
250
+ )
251
+ }
252
+
253
+ /* ─── Edit Artifact Modal ─── */
254
+
255
+ function EditArtifactModal({ item, dirName, basePath, onClose }) {
256
+ const [name, setName] = useState(item.name || '')
257
+ const [description, setDescription] = useState(item.description || '')
258
+ const [author, setAuthor] = useState(
259
+ item.author
260
+ ? (Array.isArray(item.author) ? item.author.join(', ') : item.author)
261
+ : ''
262
+ )
263
+ const [error, setError] = useState('')
264
+ const [submitting, setSubmitting] = useState(false)
265
+ const overlayRef = useRef(null)
266
+
267
+ const typeLabel = item.type === 'canvas' ? 'Canvas' : item.type === 'prototype' ? 'Prototype' : 'Component'
268
+
269
+ const handleSubmit = async (e) => {
270
+ e.preventDefault()
271
+ setError('')
272
+ setSubmitting(true)
273
+
274
+ const apiBase = (basePath || '/').replace(/\/+$/, '')
275
+ let endpoint
276
+
277
+ if (item.type === 'canvas') {
278
+ endpoint = `${apiBase}/_storyboard/canvas/update-meta`
279
+ } else if (item.type === 'prototype') {
280
+ endpoint = `${apiBase}/_storyboard/workshop/prototypes`
281
+ } else {
282
+ setError('Editing this type is not supported')
283
+ setSubmitting(false)
284
+ return
285
+ }
286
+
287
+ const body = {
288
+ name: dirName,
289
+ title: name.trim(),
290
+ description: description.trim(),
291
+ author: author.trim(),
292
+ }
293
+ if (item.folder) body.folder = item.folder
294
+
295
+ try {
296
+ const res = await fetch(endpoint, {
297
+ method: 'PUT',
298
+ headers: { 'Content-Type': 'application/json' },
299
+ body: JSON.stringify(body),
300
+ })
301
+ if (!res.ok) {
302
+ const text = await res.text()
303
+ throw new Error(text || `Request failed (${res.status})`)
304
+ }
305
+ window.location.reload()
306
+ } catch (err) {
307
+ setError(err.message)
308
+ setSubmitting(false)
309
+ }
310
+ }
311
+
312
+ useEffect(() => {
313
+ const handleKey = (e) => { if (e.key === 'Escape') onClose() }
314
+ document.addEventListener('keydown', handleKey)
315
+ return () => document.removeEventListener('keydown', handleKey)
316
+ }, [onClose])
317
+
318
+ return (
319
+ <div className={css.modalOverlay} onClick={(e) => { if (e.target === overlayRef.current) onClose() }} ref={overlayRef}>
320
+ <div className={css.modalContent} onClick={(e) => e.stopPropagation()}>
321
+ <form onSubmit={handleSubmit}>
322
+ <div className={css.createFormHeader}>
323
+ <div className={css.createMenuTitle}>Edit {typeLabel}</div>
324
+ <button type="button" className={css.createFormClose} onClick={onClose} aria-label="Close">
325
+ <XIcon size={16} />
326
+ </button>
327
+ </div>
328
+
329
+ <div className={css.createFormField}>
330
+ <label className={css.createFormLabel}>Name</label>
331
+ <input
332
+ className={css.createFormInput}
333
+ value={name}
334
+ onChange={e => setName(e.target.value)}
335
+ autoFocus
336
+ />
337
+ </div>
338
+
339
+ <div className={css.createFormField}>
340
+ <label className={css.createFormLabel}>Description</label>
341
+ <input
342
+ className={css.createFormInput}
343
+ value={description}
344
+ onChange={e => setDescription(e.target.value)}
345
+ placeholder="Optional description"
346
+ />
347
+ </div>
348
+
349
+ <div className={css.createFormField}>
350
+ <label className={css.createFormLabel}>Author</label>
351
+ <input
352
+ className={css.createFormInput}
353
+ value={author}
354
+ onChange={e => setAuthor(e.target.value)}
355
+ placeholder="GitHub username(s), comma-separated"
356
+ />
357
+ </div>
358
+
359
+ {error && <div className={css.createFormError}>{error}</div>}
360
+
361
+ <div className={css.modalActions}>
362
+ <button type="button" className={css.modalCancelBtn} onClick={onClose}>Cancel</button>
363
+ <button type="submit" className={css.modalSubmitBtn} disabled={submitting}>
364
+ {submitting ? 'Saving…' : 'Save Changes'}
365
+ </button>
366
+ </div>
367
+ </form>
368
+ </div>
369
+ </div>
370
+ )
371
+ }
372
+
373
+ /* ─── Delete Artifact Modal ─── */
374
+
375
+ function DeleteArtifactModal({ item, dirName, basePath, typeLabel, onClose, onDeleted }) {
376
+ const [error, setError] = useState('')
377
+ const [deleting, setDeleting] = useState(false)
378
+ const overlayRef = useRef(null)
379
+
380
+ const handleDelete = async () => {
381
+ setError('')
382
+ setDeleting(true)
383
+
384
+ const apiBase = (basePath || '/').replace(/\/+$/, '')
385
+ let endpoint
386
+
387
+ if (item.type === 'canvas') {
388
+ endpoint = `${apiBase}/_storyboard/canvas/delete-canvas`
389
+ } else if (item.type === 'prototype') {
390
+ endpoint = `${apiBase}/_storyboard/workshop/prototypes`
391
+ } else {
392
+ setError('Deleting this type is not supported')
393
+ setDeleting(false)
394
+ return
395
+ }
396
+
397
+ const body = { name: dirName }
398
+ if (item.folder) body.folder = item.folder
399
+
400
+ try {
401
+ const res = await fetch(endpoint, {
402
+ method: 'DELETE',
403
+ headers: { 'Content-Type': 'application/json' },
404
+ body: JSON.stringify(body),
405
+ })
406
+ if (!res.ok) {
407
+ const text = await res.text()
408
+ throw new Error(text || `Request failed (${res.status})`)
409
+ }
410
+ onDeleted?.()
411
+ onClose()
412
+ } catch (err) {
413
+ setError(err.message)
414
+ setDeleting(false)
415
+ }
416
+ }
417
+
418
+ useEffect(() => {
419
+ const handleKey = (e) => { if (e.key === 'Escape') onClose() }
420
+ document.addEventListener('keydown', handleKey)
421
+ return () => document.removeEventListener('keydown', handleKey)
422
+ }, [onClose])
423
+
424
+ return (
425
+ <div className={css.modalOverlay} onClick={(e) => { if (e.target === overlayRef.current) onClose() }} ref={overlayRef}>
426
+ <div className={css.modalContent} onClick={(e) => e.stopPropagation()}>
427
+ <div className={css.createFormHeader}>
428
+ <div className={css.createMenuTitle}>Delete {typeLabel}</div>
429
+ <button type="button" className={css.createFormClose} onClick={onClose} aria-label="Close">
430
+ <XIcon size={16} />
431
+ </button>
432
+ </div>
433
+
434
+ <p className={css.deleteMessage}>
435
+ Are you sure you want to delete <strong>{item.name}</strong>? This action cannot be undone.
436
+ </p>
437
+
438
+ {error && <div className={css.createFormError}>{error}</div>}
439
+
440
+ <div className={css.modalActions}>
441
+ <button type="button" className={css.modalCancelBtn} onClick={onClose}>Cancel</button>
442
+ <button
443
+ type="button"
444
+ className={css.deleteConfirmBtn}
445
+ onClick={handleDelete}
446
+ disabled={deleting}
447
+ >
448
+ {deleting ? 'Deleting…' : `Delete ${typeLabel}`}
449
+ </button>
450
+ </div>
451
+ </div>
452
+ </div>
453
+ )
454
+ }
455
+
456
+ /* ─── Artifact Card ─── */
457
+
458
+ function ArtifactCard({ item, basePath, starred, onToggleStar, onItemDeleted }) {
459
+ const href = item.route ? withBase(basePath, item.route) : '#'
460
+ const isExternal = item.isExternal
461
+
462
+ const handleClick = () => {
463
+ trackRecent(item.id)
464
+ }
465
+
466
+ const Tag = isExternal ? 'a' : 'a'
467
+ const linkProps = isExternal
468
+ ? { href: item.externalUrl, target: '_blank', rel: 'noopener noreferrer' }
469
+ : { href }
470
+
471
+ const authorList = item.author
472
+ ? (Array.isArray(item.author) ? item.author : [item.author])
473
+ : item.gitAuthor ? [item.gitAuthor] : []
474
+
475
+ const [showEdit, setShowEdit] = useState(false)
476
+ const [showDelete, setShowDelete] = useState(false)
477
+
478
+ // Extract dirName from item.id (format: "type:dirName")
479
+ const dirName = item.id.split(':').slice(1).join(':')
480
+ const typeLabel = item.type === 'canvas' ? 'Canvas' : item.type === 'prototype' ? 'Prototype' : 'Component'
481
+ const canEditDelete = item.type === 'canvas' || item.type === 'prototype'
482
+
483
+ return (
484
+ <>
485
+ <Tag className={css.card} {...linkProps} onClick={handleClick}>
486
+ <div className={css.cardHeader}>
487
+ <span className={css.cardBadge}>{getTypeLabel(item.type)}</span>
488
+ <div className={css.cardActions}>
489
+ <StarBtn active={starred} onClick={() => onToggleStar(item.id)} inline />
490
+ {item.flows?.length > 0 && <FlowsDropdown flows={item.flows} basePath={basePath} />}
491
+ {item.pages?.length > 1 && <PagesDropdown pages={item.pages} basePath={basePath} />}
492
+ {canEditDelete && (
493
+ <CardActionsMenu
494
+ typeLabel={typeLabel}
495
+ onEdit={() => setShowEdit(true)}
496
+ onDelete={() => setShowDelete(true)}
497
+ />
498
+ )}
499
+ </div>
500
+ </div>
501
+ <div className={css.cardBody}>
502
+ <div className={css.cardBodyContent}>
503
+ <div className={css.cardTitleRow}>
504
+ <div className={css.cardTitle}>
505
+ {item.name}
506
+ {isExternal && <span className={css.externalBadge}>↗</span>}
507
+ </div>
508
+ </div>
509
+ {item.description && (
510
+ <div className={css.cardDescription}>{item.description}</div>
511
+ )}
512
+ <div className={css.cardFooter}>
513
+ <AvatarStack authors={authorList} />
514
+ <div className={css.cardMeta}>
515
+ {authorList.length > 0 && <span>{authorList.join(', ')}</span>}
516
+ {authorList.length > 0 && formatRelativeTime(item.lastModified) && <span className={css.cardMetaDot} />}
517
+ {formatRelativeTime(item.lastModified) && <span>{formatRelativeTime(item.lastModified)}</span>}
518
+ </div>
519
+ </div>
520
+ </div>
521
+ </div>
522
+ </Tag>
523
+ {showEdit && (
524
+ <EditArtifactModal
525
+ item={item}
526
+ dirName={dirName}
527
+ basePath={basePath}
528
+ onClose={() => setShowEdit(false)}
529
+ />
530
+ )}
531
+ {showDelete && (
532
+ <DeleteArtifactModal
533
+ item={item}
534
+ dirName={dirName}
535
+ basePath={basePath}
536
+ typeLabel={typeLabel}
537
+ onClose={() => setShowDelete(false)}
538
+ onDeleted={() => onItemDeleted?.(item.id)}
539
+ />
540
+ )}
541
+ </>
542
+ )
543
+ }
544
+
545
+ function formatRelativeTime(dateStr) {
546
+ if (!dateStr) return ''
547
+ const date = new Date(dateStr)
548
+ if (isNaN(date.getTime())) return ''
549
+ const now = Date.now()
550
+ const diff = now - date.getTime()
551
+ if (diff < 0) return ''
552
+ const mins = Math.floor(diff / 60000)
553
+ if (mins < 1) return 'Just now'
554
+ if (mins < 60) return `${mins}m ago`
555
+ const hours = Math.floor(mins / 60)
556
+ if (hours < 24) return `${hours}h ago`
557
+ const days = Math.floor(hours / 24)
558
+ if (days < 7) return `${days}d ago`
559
+ if (days < 30) return `${Math.floor(days / 7)}w ago`
560
+ return date.toLocaleDateString()
561
+ }
562
+
563
+ /* ─── Flows Dropdown ─── */
564
+
565
+ function FlowsDropdown({ flows, basePath }) {
566
+ if (!flows || flows.length === 0) return null
567
+ return (
568
+ <Menu.Root>
569
+ <Menu.Trigger
570
+ className={css.iconBtn}
571
+ onClick={(e) => { e.preventDefault(); e.stopPropagation() }}
572
+ aria-label="See flows"
573
+ title="See flows"
574
+ >
575
+ <Icon name="flow" size={16} />
576
+ </Menu.Trigger>
577
+ <Menu.Portal>
578
+ <Menu.Positioner className={css.flowsPositioner} side="bottom" align="end" sideOffset={4}>
579
+ <Menu.Popup className={css.flowsPopup}>
580
+ <div className={css.flowsTitle}>Flows</div>
581
+ {flows.map(flow => (
582
+ <Menu.Item
583
+ key={flow.key}
584
+ className={css.flowsItem}
585
+ >
586
+ <a
587
+ href={withBase(basePath, flow.route)}
588
+ className={css.flowsItemLink}
589
+ >
590
+ {flow.meta?.title || flow.name}
591
+ </a>
592
+ </Menu.Item>
593
+ ))}
594
+ </Menu.Popup>
595
+ </Menu.Positioner>
596
+ </Menu.Portal>
597
+ </Menu.Root>
598
+ )
599
+ }
600
+
601
+ /* ─── Pages Dropdown ─── */
602
+
603
+ function PagesDropdown({ pages, basePath }) {
604
+ if (!pages || pages.length < 2) return null
605
+ return (
606
+ <Menu.Root>
607
+ <Menu.Trigger
608
+ className={css.iconBtn}
609
+ onClick={(e) => { e.preventDefault(); e.stopPropagation() }}
610
+ aria-label="See pages"
611
+ title="See pages"
612
+ >
613
+ <StackIcon size={16} />
614
+ </Menu.Trigger>
615
+ <Menu.Portal>
616
+ <Menu.Positioner className={css.flowsPositioner} side="bottom" align="end" sideOffset={4}>
617
+ <Menu.Popup className={css.flowsPopup}>
618
+ <div className={css.flowsTitle}>Pages</div>
619
+ {pages.map(page => (
620
+ <Menu.Item
621
+ key={page.route}
622
+ className={css.flowsItem}
623
+ >
624
+ <a
625
+ href={withBase(basePath, page.route)}
626
+ className={css.flowsItemLink}
627
+ >
628
+ {page.name}
629
+ </a>
630
+ </Menu.Item>
631
+ ))}
632
+ </Menu.Popup>
633
+ </Menu.Positioner>
634
+ </Menu.Portal>
635
+ </Menu.Root>
636
+ )
637
+ }
638
+
639
+ /* ─── Folder Section ─── */
640
+
641
+ function FolderSection({ folder, collapsed, onToggle, basePath, starred, onToggleStar, onItemDeleted }) {
642
+ return (
643
+ <section className={collapsed ? css.folderSectionCollapsed : css.folderSection}>
644
+ <button className={css.folderHeader} onClick={onToggle}>
645
+ <Icon name={collapsed ? 'folder' : 'folder-open'} size={16} className={css.folderIcon} />
646
+ <span className={css.folderName}>{folder.name}</span>
647
+ <span className={css.folderCount}>{folder.items.length}</span>
648
+ <ChevronRightIcon
649
+ size={14}
650
+ className={collapsed ? css.folderChevron : css.folderChevronExpanded}
651
+ />
652
+ </button>
653
+ {!collapsed && (
654
+ <div className={css.grid}>
655
+ {folder.items.map(item => (
656
+ <ArtifactCard
657
+ key={item.id}
658
+ item={item}
659
+ basePath={basePath}
660
+ starred={starred.has(item.id)}
661
+ onToggleStar={onToggleStar}
662
+ onItemDeleted={onItemDeleted}
663
+ />
664
+ ))}
665
+ </div>
666
+ )}
667
+ </section>
668
+ )
669
+ }
670
+
671
+ /* ─── Create Footer ─── */
672
+
673
+ function CreateTip() {
674
+ return (
675
+ <div className={css.createTip}>
676
+ <span className={css.createTipText}>
677
+ Tip: You can ask your AI assistant to create any of these artifacts: <code className={css.createTipCode}>Create a prototype</code>, <code className={css.createTipCode}>Create a canvas</code>, etc
678
+ </span>
679
+ </div>
680
+ )
681
+ }
682
+
683
+ function CreateFooter() {
684
+ return (
685
+ <div className={css.createFooter}>
686
+ <span className={css.createFooterDot} />
687
+ <span className={css.createFooterText}>Only available in dev environment</span>
688
+ </div>
689
+ )
690
+ }
691
+
692
+ /* ─── Create Form ─── */
693
+
694
+ function CreateForm({ type, onClose, basePath }) {
695
+ const [name, setName] = useState('')
696
+ const [title, setTitle] = useState('')
697
+ const [description, setDescription] = useState('')
698
+ const [url, setUrl] = useState('')
699
+ const [isExternal, setIsExternal] = useState(false)
700
+ const [prototype, setPrototype] = useState('')
701
+ const [prototypes, setPrototypes] = useState([])
702
+ const [error, setError] = useState('')
703
+ const [submitting, setSubmitting] = useState(false)
704
+
705
+ const needsPrototype = type === 'Flow' || type === 'Page'
706
+
707
+ useEffect(() => {
708
+ if (!needsPrototype) return
709
+ const apiBase = (basePath || '/').replace(/\/+$/, '')
710
+ fetch(`${apiBase}/_storyboard/workshop/flows`)
711
+ .then(r => r.ok ? r.json() : null)
712
+ .then(data => {
713
+ if (data?.prototypes) setPrototypes(data.prototypes)
714
+ })
715
+ .catch(() => {})
716
+ }, [needsPrototype, basePath])
717
+
718
+ const handleSubmit = async (e) => {
719
+ e.preventDefault()
720
+ if (!name.trim()) { setError('Name is required'); return }
721
+ if (needsPrototype && !prototype) { setError('Select a prototype'); return }
722
+ setError('')
723
+ setSubmitting(true)
724
+
725
+ const apiBase = (basePath || '/').replace(/\/+$/, '')
726
+ let endpoint, body
727
+ if (type === 'Canvas') {
728
+ endpoint = `${apiBase}/_storyboard/canvas/create`
729
+ body = { name: name.trim(), title: title.trim(), description: description.trim(), grid: true, gridSize: 24 }
730
+ } else if (type === 'Prototype') {
731
+ endpoint = `${apiBase}/_storyboard/workshop/prototypes`
732
+ body = { name: name.trim(), title: title.trim(), description: description.trim() }
733
+ if (isExternal) { body.external = true; body.url = url.trim() }
734
+ } else if (type === 'Flow') {
735
+ endpoint = `${apiBase}/_storyboard/workshop/flows`
736
+ body = { name: name.trim(), title: title.trim(), prototype, description: description.trim() }
737
+ } else if (type === 'Page') {
738
+ endpoint = `${apiBase}/_storyboard/workshop/pages`
739
+ body = { name: name.trim(), prototype }
740
+ } else {
741
+ endpoint = `${apiBase}/_storyboard/canvas/create-story`
742
+ body = { name: name.trim(), location: 'src/components' }
743
+ }
744
+
745
+ try {
746
+ const res = await fetch(endpoint, {
747
+ method: 'POST',
748
+ headers: { 'Content-Type': 'application/json' },
749
+ body: JSON.stringify(body),
750
+ })
751
+ if (!res.ok) {
752
+ const text = await res.text()
753
+ throw new Error(text || `Request failed (${res.status})`)
754
+ }
755
+ const data = await res.json().catch(() => ({}))
756
+ const route = data.route || data.path || `/${name.trim()}`
757
+ window.location.href = withBase(basePath, route)
758
+ } catch (err) {
759
+ setError(err.message)
760
+ setSubmitting(false)
761
+ }
762
+ }
763
+
764
+ const typeLabels = { Canvas: 'Canvas', Prototype: 'Prototype', Component: 'Component', Flow: 'Prototype Flow', Page: 'Prototype Page' }
765
+
766
+ return (
767
+ <form onSubmit={handleSubmit}>
768
+ <div className={css.createFormHeader}>
769
+ <div className={css.createMenuTitle}>New {typeLabels[type] || type}</div>
770
+ <button type="button" className={css.createFormClose} onClick={onClose} aria-label="Close">
771
+ <XIcon size={16} />
772
+ </button>
773
+ </div>
774
+
775
+ {needsPrototype && (
776
+ <div className={css.createFormField}>
777
+ <label className={css.createFormLabel}>Prototype *</label>
778
+ <select
779
+ className={css.createFormInput}
780
+ value={prototype}
781
+ onChange={e => setPrototype(e.target.value)}
782
+ >
783
+ <option value="">Select a prototype…</option>
784
+ {prototypes.map(p => (
785
+ <option key={p.name} value={p.name}>{p.name}</option>
786
+ ))}
787
+ </select>
788
+ </div>
789
+ )}
790
+
791
+ <div className={css.createFormField}>
792
+ <label className={css.createFormLabel}>Name *</label>
793
+ <input
794
+ className={css.createFormInput}
795
+ value={name}
796
+ onChange={e => setName(e.target.value)}
797
+ placeholder={type === 'Page' ? 'my-page' : `my-${type.toLowerCase()}`}
798
+ autoFocus={!needsPrototype}
799
+ />
800
+ </div>
801
+
802
+ {type !== 'Component' && type !== 'Page' && (
803
+ <>
804
+ <div className={css.createFormField}>
805
+ <label className={css.createFormLabel}>Title</label>
806
+ <input
807
+ className={css.createFormInput}
808
+ value={title}
809
+ onChange={e => setTitle(e.target.value)}
810
+ placeholder="Optional display title"
811
+ />
812
+ </div>
813
+ <div className={css.createFormField}>
814
+ <label className={css.createFormLabel}>Description</label>
815
+ <input
816
+ className={css.createFormInput}
817
+ value={description}
818
+ onChange={e => setDescription(e.target.value)}
819
+ placeholder="Optional description"
820
+ />
821
+ </div>
822
+ </>
823
+ )}
824
+
825
+ {type === 'Prototype' && (
826
+ <>
827
+ <div className={css.createFormField}>
828
+ <label className={css.createFormCheckbox}>
829
+ <input
830
+ type="checkbox"
831
+ checked={isExternal}
832
+ onChange={e => setIsExternal(e.target.checked)}
833
+ />
834
+ External prototype
835
+ </label>
836
+ </div>
837
+ {isExternal && (
838
+ <div className={css.createFormField}>
839
+ <label className={css.createFormLabel}>URL</label>
840
+ <input
841
+ className={css.createFormInput}
842
+ value={url}
843
+ onChange={e => setUrl(e.target.value)}
844
+ placeholder="https://example.com"
845
+ />
846
+ </div>
847
+ )}
848
+ </>
849
+ )}
850
+
851
+ {error && <div className={css.createFormError}>{error}</div>}
852
+
853
+ <div className={css.createFormActions}>
854
+ <button type="submit" className={css.createFormSubmit} disabled={submitting}>
855
+ {submitting ? 'Creating…' : `Create ${typeLabels[type] || type}`}
856
+ </button>
857
+ </div>
858
+ </form>
859
+ )
860
+ }
861
+
862
+ /* ─── Create Menu (Dropdown) ─── */
863
+
864
+ function CreateMenu({ onClose, basePath }) {
865
+ const [activeForm, setActiveForm] = useState(null)
866
+ const [showMore, setShowMore] = useState(false)
867
+
868
+ const items = [
869
+ { icon: <Icon name="canvas" size={18} />, title: 'Canvas', desc: 'Interactive board for prototypes, components, and documents' },
870
+ { icon: <Icon name="prototype" size={18} />, title: 'Prototype', desc: 'Interactive page flow' },
871
+ { icon: <Icon name="iconoir/-couple-solid" size={18} />, title: 'Component', desc: 'Reusable component' },
872
+ ]
873
+
874
+ const moreItems = [
875
+ { title: 'Prototype Flow', desc: 'A flow data file for a prototype', type: 'Flow' },
876
+ { title: 'Prototype Page', desc: 'A new page inside a prototype', type: 'Page' },
877
+ ]
878
+
879
+ if (activeForm) {
880
+ return (
881
+ <div className={css.createDropdownForm} onKeyDown={e => e.stopPropagation()}>
882
+ <CreateForm
883
+ type={activeForm}
884
+ onBack={() => setActiveForm(null)}
885
+ onClose={onClose}
886
+ basePath={basePath}
887
+ />
888
+ </div>
889
+ )
890
+ }
891
+
892
+ return (
893
+ <>
894
+ <div className={css.createDropdownTitle}>Create new artifact</div>
895
+ <div className={css.createDropdownGrid}>
896
+ {items.map(it => (
897
+ <button key={it.title} className={css.createMenuItem} onClick={() => setActiveForm(it.title)}>
898
+ <div className={css.createMenuIcon}>{it.icon}</div>
899
+ <div>
900
+ <div className={css.createMenuItemTitle}>{it.title}</div>
901
+ <div className={css.createMenuItemDesc}>{it.desc}</div>
902
+ </div>
903
+ </button>
904
+ ))}
905
+ </div>
906
+
907
+ {!showMore ? (
908
+ <button className={css.moreOptionsBtn} onClick={() => setShowMore(true)}>
909
+ More options <ChevronDownIcon size={12} />
910
+ </button>
911
+ ) : (
912
+ <div className={css.moreOptionsSection}>
913
+ {moreItems.map(it => (
914
+ <button key={it.title} className={css.moreOptionItem} onClick={() => setActiveForm(it.type)}>
915
+ <div className={css.moreOptionTitle}>{it.title}</div>
916
+ <div className={css.moreOptionDesc}>{it.desc}</div>
917
+ </button>
918
+ ))}
919
+ </div>
920
+ )}
921
+
922
+ <CreateTip />
923
+ <CreateFooter />
924
+ </>
925
+ )
926
+ }
927
+
928
+ /* ─── PAT Dialog ─── */
929
+
930
+ /* ─── Nav config ─── */
931
+
932
+ const NAV_ITEMS = [
933
+ { id: 'all', label: 'All artifacts', iconName: 'iconoir/view-grid' },
934
+ { id: 'prototypes', label: 'Prototypes', iconName: 'prototype' },
935
+ { id: 'canvases', label: 'Canvas', iconName: 'canvas' },
936
+ { id: 'components', label: 'Components', iconName: 'component' },
937
+ ]
938
+
939
+ const TAB_FILTERS = ['All', 'Recent', 'Starred']
940
+
941
+ /* ─── Branch Navigation ─── */
942
+
943
+ function BranchNav({ basePath }) {
944
+ const isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true
945
+ const { branches, currentBranch, branchBasePath } = useBranches(basePath)
946
+ const [switching, setSwitching] = useState(null)
947
+
948
+ if (!branches || branches.length === 0) return null
949
+
950
+ const branchNames = branches.map(b => b.branch)
951
+
952
+ const navigate = async (branch) => {
953
+ if (switching) return
954
+ const target = branches.find(b => b.branch === branch)
955
+ const folder = target?.folder || (branch === 'main' ? '' : `branch--${branch}/`)
956
+ const directUrl = `${branchBasePath}${folder}`
957
+
958
+ if (!isLocalDev) {
959
+ window.location.href = directUrl
960
+ return
961
+ }
962
+
963
+ // Local dev: ask server to spin up the branch, then navigate
964
+ setSwitching(branch)
965
+ const apiBase = (basePath || '/').replace(/\/$/, '')
966
+ try {
967
+ const res = await fetch(`${apiBase}/_storyboard/switch-branch`, {
968
+ method: 'POST',
969
+ headers: { 'Content-Type': 'application/json' },
970
+ body: JSON.stringify({ branch }),
971
+ })
972
+ const data = await res.json()
973
+ window.location.href = (res.ok && data.url) ? data.url : directUrl
974
+ } catch {
975
+ window.location.href = directUrl
976
+ }
977
+ }
978
+
979
+ return (
980
+ <>
981
+ <div className={css.branchNav}>
982
+ <GitBranchIcon size={14} />
983
+ <BranchSelect
984
+ branches={branchNames}
985
+ value={currentBranch}
986
+ onChange={(e) => navigate(e.target.value)}
987
+ disabled={!!switching}
988
+ />
989
+ </div>
990
+ {switching && <div className={css.switchOverlay}>
991
+ <div className={css.switchSpinner} />
992
+ <span>Starting {switching}…</span>
993
+ </div>}
994
+ </>
995
+ )
996
+ }
997
+
998
+ /* ─── User Settings Dialog ─── */
999
+
1000
+ function UserSettingsDialog({ open, onOpenChange, user, onRemoveToken }) {
1001
+ const hasToken = (() => {
1002
+ try { return !!localStorage.getItem(COMMENTS_TOKEN_KEY) } catch { return false }
1003
+ })()
1004
+ const scopes = user?.scopes || []
1005
+ const isFineGrained = hasToken && scopes.length === 0
1006
+
1007
+ return (
1008
+ <Dialog.Root open={open} onOpenChange={onOpenChange}>
1009
+ <Dialog.Portal>
1010
+ <Dialog.Backdrop className={css.settingsBackdrop} />
1011
+ <div className={css.settingsPopupWrap}>
1012
+ <Dialog.Popup className={css.settingsPopup}>
1013
+ <Dialog.Title className={css.settingsTitle}>Settings</Dialog.Title>
1014
+ <Dialog.Close className={css.settingsCloseBtn} aria-label="Close">×</Dialog.Close>
1015
+
1016
+ {/* GitHub connection section */}
1017
+ <div className={css.settingsSection}>
1018
+ <div className={css.settingsSectionHeader}>
1019
+ <ShieldLockIcon size={16} />
1020
+ <span>GitHub Connection</span>
1021
+ </div>
1022
+
1023
+ {hasToken ? (
1024
+ <div className={css.settingsTokenCard}>
1025
+ <div className={css.settingsTokenRow}>
1026
+ <span className={css.settingsTokenLabel}>Token</span>
1027
+ <code className={css.settingsTokenValue}>••••••••••••••••</code>
1028
+ </div>
1029
+ <div className={css.settingsTokenRow}>
1030
+ <span className={css.settingsTokenLabel}>Permissions</span>
1031
+ <span className={css.settingsTokenValue}>
1032
+ {isFineGrained
1033
+ ? 'Fine-grained token'
1034
+ : scopes.map(s => <code key={s} className={css.settingsScope}>{s}</code>)
1035
+ }
1036
+ </span>
1037
+ </div>
1038
+ <button className={css.settingsRemoveBtn} onClick={onRemoveToken}>
1039
+ <TrashIcon size={14} />
1040
+ Remove token
1041
+ </button>
1042
+ </div>
1043
+ ) : (
1044
+ <div className={css.settingsNoToken}>
1045
+ <p>No GitHub token configured.</p>
1046
+ <button
1047
+ className={css.settingsSignInBtn}
1048
+ onClick={() => {
1049
+ onOpenChange(false)
1050
+ document.dispatchEvent(new CustomEvent('storyboard:open-auth-modal'))
1051
+ }}
1052
+ >
1053
+ <MarkGithubIcon size={16} />
1054
+ Sign in with GitHub
1055
+ </button>
1056
+ </div>
1057
+ )}
1058
+ </div>
1059
+ </Dialog.Popup>
1060
+ </div>
1061
+ </Dialog.Portal>
1062
+ </Dialog.Root>
1063
+ )
1064
+ }
1065
+
1066
+ /* ─── Main Component ─── */
1067
+
1068
+ export default function Workspace({
1069
+ pageModules = {},
1070
+ basePath,
1071
+ title = 'Storyboard',
1072
+ subtitle,
1073
+ }) {
1074
+ const themeAttrs = useToolbarTheme()
1075
+ const ghUser = useGitHubUser(basePath)
1076
+ const [settingsOpen, setSettingsOpen] = useState(false)
1077
+
1078
+ const handleRemoveToken = useCallback(() => {
1079
+ try {
1080
+ localStorage.removeItem(COMMENTS_TOKEN_KEY)
1081
+ localStorage.removeItem(COMMENTS_USER_KEY)
1082
+ } catch { /* ignore */ }
1083
+ document.dispatchEvent(new CustomEvent('storyboard:auth-changed'))
1084
+ setSettingsOpen(false)
1085
+ }, [])
1086
+
1087
+ // Build data index from real prototype/canvas/story data
1088
+ const knownRoutes = useMemo(() =>
1089
+ Object.keys(pageModules)
1090
+ .map(p => p.replace('/src/prototypes/', '').replace('.jsx', ''))
1091
+ .filter(n => !n.startsWith('_') && n !== 'index' && n !== 'workspace' && n !== 'viewfinder'),
1092
+ [pageModules],
1093
+ )
1094
+
1095
+ const prototypeIndex = useMemo(() => buildPrototypeIndex(knownRoutes), [knownRoutes])
1096
+
1097
+ // Build unified items list from all sources
1098
+ const allItems = useMemo(() => {
1099
+ const items = []
1100
+
1101
+ // Prototypes (ungrouped + from folders)
1102
+ const addProto = (proto) => {
1103
+ // Prefer a flow marked as default, fall back to the first flow
1104
+ const defaultFlow = proto.flows?.find(f => f.meta?.default === true)
1105
+ const route = defaultFlow
1106
+ ? defaultFlow.route
1107
+ : proto.flows?.length > 0
1108
+ ? proto.flows[0].route
1109
+ : `/${proto.dirName}`
1110
+
1111
+ items.push({
1112
+ id: `proto:${proto.dirName}`,
1113
+ name: proto.name,
1114
+ type: 'prototype',
1115
+ author: proto.author,
1116
+ gitAuthor: proto.gitAuthor,
1117
+ lastModified: proto.lastModified,
1118
+ route,
1119
+ isExternal: proto.isExternal,
1120
+ externalUrl: proto.externalUrl,
1121
+ folder: proto.folder,
1122
+ description: proto.description,
1123
+ flows: proto.flows || [],
1124
+ })
1125
+ }
1126
+
1127
+ for (const proto of prototypeIndex.prototypes || []) addProto(proto)
1128
+ for (const folder of prototypeIndex.folders || []) {
1129
+ for (const proto of folder.prototypes || []) addProto(proto)
1130
+ }
1131
+
1132
+ // Canvases (ungrouped + from folders)
1133
+ const addCanvas = (canvas) => {
1134
+ items.push({
1135
+ id: `canvas:${canvas.dirName}`,
1136
+ name: canvas.name,
1137
+ type: 'canvas',
1138
+ author: canvas.author,
1139
+ gitAuthor: canvas.gitAuthor,
1140
+ lastModified: null,
1141
+ route: canvas.route,
1142
+ isExternal: false,
1143
+ externalUrl: null,
1144
+ folder: canvas.folder,
1145
+ description: canvas.description,
1146
+ pages: canvas.pages || null,
1147
+ })
1148
+ }
1149
+
1150
+ for (const canvas of prototypeIndex.canvases || []) addCanvas(canvas)
1151
+ for (const folder of prototypeIndex.folders || []) {
1152
+ for (const canvas of folder.canvases || []) addCanvas(canvas)
1153
+ }
1154
+
1155
+ // Components (stories)
1156
+ const storyNames = listStories()
1157
+ for (const name of storyNames) {
1158
+ const data = getStoryData(name)
1159
+ if (!data) continue
1160
+ items.push({
1161
+ id: `component:${name}`,
1162
+ name: name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
1163
+ type: 'component',
1164
+ author: null,
1165
+ gitAuthor: null,
1166
+ lastModified: null,
1167
+ route: data._route || `/components/${name}`,
1168
+ isExternal: false,
1169
+ externalUrl: null,
1170
+ folder: null,
1171
+ description: null,
1172
+ })
1173
+ }
1174
+
1175
+ return items
1176
+ }, [prototypeIndex])
1177
+
1178
+ const itemMap = useMemo(() => Object.fromEntries(allItems.map(i => [i.id, i])), [allItems])
1179
+
1180
+ // State
1181
+ const [activeNav, setActiveNav] = useState('all')
1182
+ const [activeTab, setActiveTab] = useState('All')
1183
+ const [showCreate, setShowCreate] = useState(false)
1184
+ const isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true
1185
+ const [sidebarOpen, setSidebarOpen] = useState(false)
1186
+ const [groupByFolders, setGroupByFolders] = useState(() => {
1187
+ try { return localStorage.getItem(GROUP_BY_FOLDERS_KEY) !== 'false' } catch { return true }
1188
+ })
1189
+ const [collapsedFolders, setCollapsedFolders] = useState(new Set())
1190
+ const [hiddenItems, setHiddenItems] = useState(new Set())
1191
+ const { starred, toggle: toggleStar } = useStarred()
1192
+ const recentIds = useRecent()
1193
+
1194
+ // Filter by nav category
1195
+ const typeMap = { prototypes: 'prototype', canvases: 'canvas', components: 'component' }
1196
+ const navFiltered = useMemo(() => {
1197
+ let filtered = activeNav === 'all' ? allItems : allItems.filter(i => i.type === typeMap[activeNav])
1198
+ if (hiddenItems.size > 0) filtered = filtered.filter(i => !hiddenItems.has(i.id))
1199
+ return filtered
1200
+ }, [allItems, activeNav, hiddenItems])
1201
+
1202
+ // Filter by tab
1203
+ const items = useMemo(() => {
1204
+ if (activeTab === 'Recent') {
1205
+ const ordered = recentIds.map(id => itemMap[id]).filter(Boolean)
1206
+ if (activeNav !== 'all') {
1207
+ const typeMap = { prototypes: 'prototype', canvases: 'canvas', components: 'component' }
1208
+ return ordered.filter(i => i.type === typeMap[activeNav])
1209
+ }
1210
+ return ordered
1211
+ }
1212
+ const base = activeTab === 'Starred'
1213
+ ? navFiltered.filter(i => starred.has(i.id))
1214
+ : navFiltered
1215
+ return [...base].sort((a, b) => {
1216
+ const aTime = a.lastModified ? new Date(a.lastModified).getTime() : 0
1217
+ const bTime = b.lastModified ? new Date(b.lastModified).getTime() : 0
1218
+ return bTime - aTime
1219
+ })
1220
+ }, [activeTab, activeNav, navFiltered, recentIds, itemMap, starred])
1221
+
1222
+ // Grouped items for folder view
1223
+ const grouped = useMemo(() => {
1224
+ if (!groupByFolders) return null
1225
+ const folderItems = {}
1226
+ const ungrouped = []
1227
+ for (const item of items) {
1228
+ if (item.folder) {
1229
+ if (!folderItems[item.folder]) folderItems[item.folder] = []
1230
+ folderItems[item.folder].push(item)
1231
+ } else {
1232
+ ungrouped.push(item)
1233
+ }
1234
+ }
1235
+ const folderMeta = {}
1236
+ for (const f of prototypeIndex.folders || []) folderMeta[f.dirName] = f
1237
+ const folders = Object.entries(folderItems).map(([dirName, fItems]) => ({
1238
+ dirName,
1239
+ name: folderMeta[dirName]?.name || dirName,
1240
+ items: fItems,
1241
+ }))
1242
+ folders.sort((a, b) => {
1243
+ const aMax = Math.max(0, ...a.items.map(i => i.lastModified ? new Date(i.lastModified).getTime() : 0))
1244
+ const bMax = Math.max(0, ...b.items.map(i => i.lastModified ? new Date(i.lastModified).getTime() : 0))
1245
+ return bMax - aMax
1246
+ })
1247
+ return { ungrouped, folders }
1248
+ }, [items, groupByFolders, prototypeIndex])
1249
+
1250
+ const toggleGrouping = useCallback(() => {
1251
+ setGroupByFolders(prev => {
1252
+ const next = !prev
1253
+ try { localStorage.setItem(GROUP_BY_FOLDERS_KEY, String(next)) } catch { /* empty */ }
1254
+ return next
1255
+ })
1256
+ }, [])
1257
+
1258
+ const toggleFolder = useCallback((dirName) => {
1259
+ setCollapsedFolders(prev => {
1260
+ const next = new Set(prev)
1261
+ if (next.has(dirName)) next.delete(dirName)
1262
+ else next.add(dirName)
1263
+ return next
1264
+ })
1265
+ }, [])
1266
+
1267
+ const handleItemDeleted = useCallback((itemId) => {
1268
+ setHiddenItems(prev => new Set(prev).add(itemId))
1269
+ }, [])
1270
+
1271
+ // Counts
1272
+ const visibleItems = useMemo(() =>
1273
+ hiddenItems.size > 0 ? allItems.filter(i => !hiddenItems.has(i.id)) : allItems
1274
+ , [allItems, hiddenItems])
1275
+
1276
+ const counts = useMemo(() => ({
1277
+ all: visibleItems.length,
1278
+ prototypes: visibleItems.filter(i => i.type === 'prototype').length,
1279
+ canvases: visibleItems.filter(i => i.type === 'canvas').length,
1280
+ components: visibleItems.filter(i => i.type === 'component').length,
1281
+ }), [visibleItems])
1282
+
1283
+ // Starred items for sidebar
1284
+ const starredItems = useMemo(() => visibleItems.filter(i => starred.has(i.id)), [visibleItems, starred])
1285
+
1286
+ return (
1287
+ <div className={css.layout} {...themeAttrs}>
1288
+ {/* ─── Full-width Header ─── */}
1289
+ <header className={css.topBar}>
1290
+ <div className={css.topBarLeft}>
1291
+ <button
1292
+ className={css.hamburgerBtn}
1293
+ onClick={() => setSidebarOpen(prev => !prev)}
1294
+ aria-label="Toggle menu"
1295
+ >
1296
+ {sidebarOpen ? <XIcon size={18} /> : <ThreeBarsIcon size={18} />}
1297
+ </button>
1298
+ <div className={`${css.logo} smooth-corners`}><Icon name="iconoir/key-command" size={22} color="#fff" /></div>
1299
+ <div>
1300
+ <div className={css.appName}>{title}</div>
1301
+ {subtitle && <div className={css.appSubtitle}>{subtitle}</div>}
1302
+ </div>
1303
+ </div>
1304
+ <div className={css.topActions}>
1305
+ <BranchNav basePath={basePath} />
1306
+ {isLocalDev && (
1307
+ <Menu.Root open={showCreate} onOpenChange={setShowCreate}>
1308
+ <Menu.Trigger className={css.createBtn}>
1309
+ <PlusIcon size={14} /> Create
1310
+ </Menu.Trigger>
1311
+ <Menu.Portal>
1312
+ <Menu.Positioner className={css.createDropdownPositioner} side="bottom" align="end" sideOffset={4}>
1313
+ <Menu.Popup className={css.createDropdown}>
1314
+ <CreateMenu onClose={() => setShowCreate(false)} basePath={basePath} />
1315
+ </Menu.Popup>
1316
+ </Menu.Positioner>
1317
+ </Menu.Portal>
1318
+ </Menu.Root>
1319
+ )}
1320
+ </div>
1321
+ </header>
1322
+
1323
+ {/* ─── Body: Sidebar + Content ─── */}
1324
+ <div className={css.body}>
1325
+ {/* ─── Sidebar ─── */}
1326
+ <aside className={`${css.sidebar}${sidebarOpen ? ` ${css.sidebarOpen}` : ''}`}>
1327
+ <div className={css.sidebarContent}>
1328
+ <nav className={css.navSection}>
1329
+ {NAV_ITEMS.map(nav => (
1330
+ <button
1331
+ key={nav.id}
1332
+ className={activeNav === nav.id ? css.navItemActive : css.navItem}
1333
+ onClick={() => { setActiveNav(nav.id); setSidebarOpen(false) }}
1334
+ >
1335
+ <span className={css.navIcon}><Icon name={nav.iconName} size={16} /></span>
1336
+ {nav.label}
1337
+ <span className={css.navCount}>{counts[nav.id]}</span>
1338
+ </button>
1339
+ ))}
1340
+ </nav>
1341
+
1342
+ <div className={css.separator} />
1343
+
1344
+ <div className={css.sectionLabel}>Starred</div>
1345
+ {starredItems.length === 0 && (
1346
+ <div className={css.starredEmpty}>Star items to pin them here</div>
1347
+ )}
1348
+ {starredItems.map(s => (
1349
+ <a
1350
+ key={s.id}
1351
+ className={css.starredItem}
1352
+ href={s.isExternal ? s.externalUrl : withBase(basePath, s.route)}
1353
+ {...(s.isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
1354
+ onClick={() => trackRecent(s.id)}
1355
+ >
1356
+ <span className={css.starredIcon}>{getTypeIcon(s.type)}</span>
1357
+ {s.name}
1358
+ </a>
1359
+ ))}
1360
+ </div>
1361
+
1362
+ {/* User profile / settings */}
1363
+ <div className={css.sidebarFooter}>
1364
+ {ghUser ? (
1365
+ <div className={css.footerRow}>
1366
+ <button className={css.userBtn} onClick={() => setSettingsOpen(true)}>
1367
+ <img
1368
+ className={css.userAvatar}
1369
+ src={ghUser.avatarUrl || `https://github.com/${ghUser.login}.png?size=64`}
1370
+ alt={ghUser.login}
1371
+ width={32}
1372
+ height={32}
1373
+ />
1374
+ <div className={css.userInfo}>
1375
+ <div className={css.userName}>{ghUser.login}</div>
1376
+ </div>
1377
+ </button>
1378
+ </div>
1379
+ ) : (
1380
+ <div className={css.footerRow}>
1381
+ <button className={css.loginBtn} onClick={() => document.dispatchEvent(new CustomEvent('storyboard:open-auth-modal'))}>
1382
+ <span className={css.avatar}><MarkGithubIcon size={16} /></span>
1383
+ <div>
1384
+ <div className={css.userName}>Sign in</div>
1385
+ <div className={css.userSub}>Connect with GitHub</div>
1386
+ </div>
1387
+ </button>
1388
+ </div>
1389
+ )}
1390
+ </div>
1391
+ <UserSettingsDialog
1392
+ open={settingsOpen}
1393
+ onOpenChange={setSettingsOpen}
1394
+ user={ghUser}
1395
+ onRemoveToken={handleRemoveToken}
1396
+ />
1397
+ </aside>
1398
+
1399
+ {/* ─── Main ─── */}
1400
+ <main className={css.main}>
1401
+ {/* Tabs */}
1402
+ <div className={css.tabs}>
1403
+ {TAB_FILTERS.map(t => (
1404
+ <button
1405
+ key={t}
1406
+ className={activeTab === t ? css.tabActive : css.tab}
1407
+ onClick={() => setActiveTab(t)}
1408
+ >
1409
+ {t}
1410
+ </button>
1411
+ ))}
1412
+ <label className={css.groupByFolders}>
1413
+ <input
1414
+ type="checkbox"
1415
+ className={css.groupByFoldersCheckbox}
1416
+ checked={groupByFolders}
1417
+ onChange={toggleGrouping}
1418
+ />
1419
+ Group by folders
1420
+ </label>
1421
+ </div>
1422
+
1423
+ {/* Grid */}
1424
+ <div className={css.content}>
1425
+ {items.length === 0 ? (
1426
+ <div className={css.emptyState}>
1427
+ {activeTab === 'Recent' && 'No recently opened items yet.'}
1428
+ {activeTab === 'Starred' && 'No starred items. Click ☆ on a card to star it.'}
1429
+ {activeTab === 'All' && 'No items found. Create a prototype, canvas, or component to get started.'}
1430
+ </div>
1431
+ ) : groupByFolders && grouped && activeTab === 'All' ? (
1432
+ <>
1433
+ {grouped.folders.map(folder => (
1434
+ <FolderSection
1435
+ key={folder.dirName}
1436
+ folder={folder}
1437
+ collapsed={collapsedFolders.has(folder.dirName)}
1438
+ onToggle={() => toggleFolder(folder.dirName)}
1439
+ basePath={basePath}
1440
+ starred={starred}
1441
+ onToggleStar={toggleStar}
1442
+ onItemDeleted={handleItemDeleted}
1443
+ />
1444
+ ))}
1445
+ {grouped.ungrouped.length > 0 && (
1446
+ <div className={css.grid}>
1447
+ {grouped.ungrouped.map(item => (
1448
+ <ArtifactCard
1449
+ key={item.id}
1450
+ item={item}
1451
+ basePath={basePath}
1452
+ starred={starred.has(item.id)}
1453
+ onToggleStar={toggleStar}
1454
+ onItemDeleted={handleItemDeleted}
1455
+ />
1456
+ ))}
1457
+ </div>
1458
+ )}
1459
+ </>
1460
+ ) : (
1461
+ <div className={css.grid}>
1462
+ {items.map(item => (
1463
+ <ArtifactCard
1464
+ key={item.id}
1465
+ item={item}
1466
+ basePath={basePath}
1467
+ starred={starred.has(item.id)}
1468
+ onToggleStar={toggleStar}
1469
+ onItemDeleted={handleItemDeleted}
1470
+ />
1471
+ ))}
1472
+ </div>
1473
+ )}
1474
+ </div>
1475
+ </main>
1476
+ </div>
1477
+ </div>
1478
+ )
1479
+ }