@polderlabs/bizar 7.0.2 → 8.0.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 (490) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/bizar-dash/DESIGN.md +729 -969
  3. package/bizar-dash/GLYPH.md +418 -0
  4. package/bizar-dash/PLAN.md +1135 -0
  5. package/bizar-dash/dist/assets/ActivityFeed-BRBtQOHB.js +6 -0
  6. package/bizar-dash/dist/assets/ActivityFeed-BRBtQOHB.js.map +1 -0
  7. package/bizar-dash/dist/assets/ActivityView-CwqhxBfh.js +1 -0
  8. package/bizar-dash/dist/assets/ActivityView-CwqhxBfh.js.map +1 -0
  9. package/bizar-dash/dist/assets/AgentActivity-6MxU-a9F.js +1 -0
  10. package/bizar-dash/dist/assets/AgentActivity-6MxU-a9F.js.map +1 -0
  11. package/bizar-dash/dist/assets/AgentsView-BYrOFdwl.js +8 -0
  12. package/bizar-dash/dist/assets/AgentsView-BYrOFdwl.js.map +1 -0
  13. package/bizar-dash/dist/assets/Badge-Cd61wsBa.js +1 -0
  14. package/bizar-dash/dist/assets/Badge-Cd61wsBa.js.map +1 -0
  15. package/bizar-dash/dist/assets/Card-C_bvqXdQ.js +1 -0
  16. package/bizar-dash/dist/assets/Card-C_bvqXdQ.js.map +1 -0
  17. package/bizar-dash/dist/assets/EmptyState-BWQuKnk9.js +1 -0
  18. package/bizar-dash/dist/assets/EmptyState-BWQuKnk9.js.map +1 -0
  19. package/bizar-dash/dist/assets/GoalsView-Dok0yt9S.js +11 -0
  20. package/bizar-dash/dist/assets/GoalsView-Dok0yt9S.js.map +1 -0
  21. package/bizar-dash/dist/assets/Grid-C62gCPqt.js +1 -0
  22. package/bizar-dash/dist/assets/Grid-C62gCPqt.js.map +1 -0
  23. package/bizar-dash/dist/assets/KanbanCard-CtTKP6ZX.js +21 -0
  24. package/bizar-dash/dist/assets/KanbanCard-CtTKP6ZX.js.map +1 -0
  25. package/bizar-dash/dist/assets/LibrariesView-BcyBQXiY.js +67 -0
  26. package/bizar-dash/dist/assets/LibrariesView-BcyBQXiY.js.map +1 -0
  27. package/bizar-dash/dist/assets/MemoryView-49wR4u-y.js +13 -0
  28. package/bizar-dash/dist/assets/MemoryView-49wR4u-y.js.map +1 -0
  29. package/bizar-dash/dist/assets/OverviewView-BtQ_UWYj.js +1 -0
  30. package/bizar-dash/dist/assets/OverviewView-BtQ_UWYj.js.map +1 -0
  31. package/bizar-dash/dist/assets/ProgressBar-CpvDeq9F.js +1 -0
  32. package/bizar-dash/dist/assets/ProgressBar-CpvDeq9F.js.map +1 -0
  33. package/bizar-dash/dist/assets/SettingsRow-DmIe5OUu.js +30 -0
  34. package/bizar-dash/dist/assets/SettingsRow-DmIe5OUu.js.map +1 -0
  35. package/bizar-dash/dist/assets/SettingsView-D1KmGWbi.js +48 -0
  36. package/bizar-dash/dist/assets/SettingsView-D1KmGWbi.js.map +1 -0
  37. package/bizar-dash/dist/assets/StatTile-BkIP32gG.js +11 -0
  38. package/bizar-dash/dist/assets/StatTile-BkIP32gG.js.map +1 -0
  39. package/bizar-dash/dist/assets/TasksView-Cba3GJ42.js +1 -0
  40. package/bizar-dash/dist/assets/TasksView-Cba3GJ42.js.map +1 -0
  41. package/bizar-dash/dist/assets/ViewHeader-D6c2cw1b.js +6 -0
  42. package/bizar-dash/dist/assets/ViewHeader-D6c2cw1b.js.map +1 -0
  43. package/bizar-dash/dist/assets/brain-DeoXNDE2.js +6 -0
  44. package/bizar-dash/dist/assets/brain-DeoXNDE2.js.map +1 -0
  45. package/bizar-dash/dist/assets/calendar-DRlQkGJc.js +6 -0
  46. package/bizar-dash/dist/assets/calendar-DRlQkGJc.js.map +1 -0
  47. package/bizar-dash/dist/assets/circle-check-DmYQ3vGp.js +6 -0
  48. package/bizar-dash/dist/assets/circle-check-DmYQ3vGp.js.map +1 -0
  49. package/bizar-dash/dist/assets/cmdk-BwN8hcKQ.js +80 -0
  50. package/bizar-dash/dist/assets/cmdk-BwN8hcKQ.js.map +1 -0
  51. package/bizar-dash/dist/assets/dnd-kit-DR2Qzzxk.js +5 -0
  52. package/bizar-dash/dist/assets/dnd-kit-DR2Qzzxk.js.map +1 -0
  53. package/bizar-dash/dist/assets/git-merge-OoqBKQer.js +6 -0
  54. package/bizar-dash/dist/assets/git-merge-OoqBKQer.js.map +1 -0
  55. package/bizar-dash/dist/assets/git-pull-request-CXBQTR2f.js +6 -0
  56. package/bizar-dash/dist/assets/git-pull-request-CXBQTR2f.js.map +1 -0
  57. package/bizar-dash/dist/assets/main-COdgjqbZ.css +1 -0
  58. package/bizar-dash/dist/assets/main-Cw6gybzS.js +109 -0
  59. package/bizar-dash/dist/assets/main-Cw6gybzS.js.map +1 -0
  60. package/bizar-dash/dist/assets/message-square-DUTeIQAR.js +6 -0
  61. package/bizar-dash/dist/assets/message-square-DUTeIQAR.js.map +1 -0
  62. package/bizar-dash/dist/assets/minus-CrStJTZ7.js +6 -0
  63. package/bizar-dash/dist/assets/minus-CrStJTZ7.js.map +1 -0
  64. package/bizar-dash/dist/assets/wrench-BJ1WIC4u.js +6 -0
  65. package/bizar-dash/dist/assets/wrench-BJ1WIC4u.js.map +1 -0
  66. package/bizar-dash/dist/{mobile.html → v8.html} +6 -9
  67. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  68. package/bizar-dash/src/server/server.mjs +1 -57
  69. package/bizar-dash/src/web/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  70. package/bizar-dash/src/web/v8/App.tsx +188 -0
  71. package/bizar-dash/src/web/v8/__tests__/App.test.tsx +102 -0
  72. package/bizar-dash/src/web/v8/__tests__/AppShell.test.tsx +79 -0
  73. package/bizar-dash/src/web/v8/__tests__/PageSkeleton.test.tsx +12 -0
  74. package/bizar-dash/src/web/v8/__tests__/SettingsView.test.tsx +84 -0
  75. package/bizar-dash/src/web/v8/__tests__/Sidebar.test.tsx +95 -0
  76. package/bizar-dash/src/web/v8/__tests__/activity.test.tsx +63 -0
  77. package/bizar-dash/src/web/v8/__tests__/agents.test.tsx +90 -0
  78. package/bizar-dash/src/web/v8/__tests__/controls.test.tsx +135 -0
  79. package/bizar-dash/src/web/v8/__tests__/cx.test.ts +25 -0
  80. package/bizar-dash/src/web/v8/__tests__/data.test.tsx +293 -0
  81. package/bizar-dash/src/web/v8/__tests__/feedback.test.tsx +213 -0
  82. package/bizar-dash/src/web/v8/__tests__/goals.test.tsx +105 -0
  83. package/bizar-dash/src/web/v8/__tests__/kanban.test.tsx +176 -0
  84. package/bizar-dash/src/web/v8/__tests__/libraries.test.tsx +70 -0
  85. package/bizar-dash/src/web/v8/__tests__/memory.test.tsx +60 -0
  86. package/bizar-dash/src/web/v8/__tests__/navigation.test.tsx +137 -0
  87. package/bizar-dash/src/web/v8/__tests__/settings.test.tsx +104 -0
  88. package/bizar-dash/src/web/v8/__tests__/theme.test.tsx +98 -0
  89. package/bizar-dash/src/web/v8/__tests__/views.test.tsx +112 -0
  90. package/bizar-dash/src/web/v8/main.tsx +28 -0
  91. package/bizar-dash/src/web/v8/shell/AppShell.tsx +90 -0
  92. package/bizar-dash/src/web/v8/shell/PageSkeleton.tsx +29 -0
  93. package/bizar-dash/src/web/v8/shell/Sidebar.tsx +361 -0
  94. package/bizar-dash/src/web/v8/shell/StatusBar.tsx +63 -0
  95. package/bizar-dash/src/web/v8/shell/Topbar.tsx +113 -0
  96. package/bizar-dash/src/web/v8/shell/index.ts +15 -0
  97. package/bizar-dash/src/web/v8/ui/activity/ActivityFeed.tsx +137 -0
  98. package/bizar-dash/src/web/v8/ui/agents/AgentActivity.tsx +101 -0
  99. package/bizar-dash/src/web/v8/ui/agents/AgentCard.tsx +158 -0
  100. package/bizar-dash/src/web/v8/ui/controls/Button.tsx +141 -0
  101. package/bizar-dash/src/web/v8/ui/controls/ButtonGroup.tsx +32 -0
  102. package/bizar-dash/src/web/v8/ui/controls/Checkbox.tsx +49 -0
  103. package/bizar-dash/src/web/v8/ui/controls/Field.tsx +78 -0
  104. package/bizar-dash/src/web/v8/ui/controls/Form.tsx +23 -0
  105. package/bizar-dash/src/web/v8/ui/controls/IconButton.tsx +112 -0
  106. package/bizar-dash/src/web/v8/ui/controls/Input.tsx +94 -0
  107. package/bizar-dash/src/web/v8/ui/controls/RadioGroup.tsx +74 -0
  108. package/bizar-dash/src/web/v8/ui/controls/Select.tsx +171 -0
  109. package/bizar-dash/src/web/v8/ui/controls/Slider.tsx +68 -0
  110. package/bizar-dash/src/web/v8/ui/controls/Switch.tsx +56 -0
  111. package/bizar-dash/src/web/v8/ui/controls/Textarea.tsx +64 -0
  112. package/bizar-dash/src/web/v8/ui/controls/Toggle.tsx +36 -0
  113. package/bizar-dash/src/web/v8/ui/controls/ToggleGroup.tsx +85 -0
  114. package/bizar-dash/src/web/v8/ui/data/Accordion.tsx +134 -0
  115. package/bizar-dash/src/web/v8/ui/data/Avatar.tsx +175 -0
  116. package/bizar-dash/src/web/v8/ui/data/Badge.tsx +93 -0
  117. package/bizar-dash/src/web/v8/ui/data/BarList.tsx +101 -0
  118. package/bizar-dash/src/web/v8/ui/data/Card.tsx +140 -0
  119. package/bizar-dash/src/web/v8/ui/data/Chip.tsx +96 -0
  120. package/bizar-dash/src/web/v8/ui/data/Kbd.tsx +42 -0
  121. package/bizar-dash/src/web/v8/ui/data/ProgressBar.tsx +93 -0
  122. package/bizar-dash/src/web/v8/ui/data/Sparkline.tsx +98 -0
  123. package/bizar-dash/src/web/v8/ui/data/StatTile.tsx +150 -0
  124. package/bizar-dash/src/web/v8/ui/data/Table.tsx +170 -0
  125. package/bizar-dash/src/web/v8/ui/data/Timeline.tsx +130 -0
  126. package/bizar-dash/src/web/v8/ui/data/ViewHeader.tsx +122 -0
  127. package/bizar-dash/src/web/v8/ui/feedback/Alert.tsx +95 -0
  128. package/bizar-dash/src/web/v8/ui/feedback/Banner.tsx +39 -0
  129. package/bizar-dash/src/web/v8/ui/feedback/ContextMenu.tsx +143 -0
  130. package/bizar-dash/src/web/v8/ui/feedback/Dialog.tsx +149 -0
  131. package/bizar-dash/src/web/v8/ui/feedback/Drawer.tsx +8 -0
  132. package/bizar-dash/src/web/v8/ui/feedback/DropdownMenu.tsx +231 -0
  133. package/bizar-dash/src/web/v8/ui/feedback/EmptyState.tsx +48 -0
  134. package/bizar-dash/src/web/v8/ui/feedback/Popover.tsx +56 -0
  135. package/bizar-dash/src/web/v8/ui/feedback/Sheet.tsx +103 -0
  136. package/bizar-dash/src/web/v8/ui/feedback/Skeleton.tsx +67 -0
  137. package/bizar-dash/src/web/v8/ui/feedback/Spinner.tsx +30 -0
  138. package/bizar-dash/src/web/v8/ui/feedback/Toast.tsx +66 -0
  139. package/bizar-dash/src/web/v8/ui/feedback/Tooltip.tsx +89 -0
  140. package/bizar-dash/src/web/v8/ui/goals/GoalCard.tsx +176 -0
  141. package/bizar-dash/src/web/v8/ui/goals/KeyResult.tsx +115 -0
  142. package/bizar-dash/src/web/v8/ui/index.ts +322 -0
  143. package/bizar-dash/src/web/v8/ui/kanban/KanbanBoard.tsx +80 -0
  144. package/bizar-dash/src/web/v8/ui/kanban/KanbanCard.tsx +247 -0
  145. package/bizar-dash/src/web/v8/ui/kanban/KanbanColumn.tsx +181 -0
  146. package/bizar-dash/src/web/v8/ui/kanban/KanbanContextMenu.tsx +122 -0
  147. package/bizar-dash/src/web/v8/ui/kanban/KanbanQuickAdd.tsx +141 -0
  148. package/bizar-dash/src/web/v8/ui/libraries/LibraryGrid.tsx +30 -0
  149. package/bizar-dash/src/web/v8/ui/libraries/LibraryItem.tsx +161 -0
  150. package/bizar-dash/src/web/v8/ui/memory/MemoryVault.tsx +144 -0
  151. package/bizar-dash/src/web/v8/ui/navigation/CommandPalette.tsx +224 -0
  152. package/bizar-dash/src/web/v8/ui/navigation/NavLink.tsx +81 -0
  153. package/bizar-dash/src/web/v8/ui/navigation/Pagination.tsx +164 -0
  154. package/bizar-dash/src/web/v8/ui/navigation/Tabs.tsx +128 -0
  155. package/bizar-dash/src/web/v8/ui/navigation/useCommandPaletteHotkey.test.ts +79 -0
  156. package/bizar-dash/src/web/v8/ui/primitives/Box.tsx +144 -0
  157. package/bizar-dash/src/web/v8/ui/primitives/Center.tsx +40 -0
  158. package/bizar-dash/src/web/v8/ui/primitives/Cluster.tsx +67 -0
  159. package/bizar-dash/src/web/v8/ui/primitives/Grid.tsx +104 -0
  160. package/bizar-dash/src/web/v8/ui/primitives/Inline.tsx +77 -0
  161. package/bizar-dash/src/web/v8/ui/primitives/Portal.tsx +33 -0
  162. package/bizar-dash/src/web/v8/ui/primitives/ScrollArea.tsx +62 -0
  163. package/bizar-dash/src/web/v8/ui/primitives/Separator.tsx +42 -0
  164. package/bizar-dash/src/web/v8/ui/primitives/Stack.tsx +73 -0
  165. package/bizar-dash/src/web/v8/ui/primitives/VisuallyHidden.tsx +49 -0
  166. package/bizar-dash/src/web/v8/ui/settings/SettingsNav.tsx +91 -0
  167. package/bizar-dash/src/web/v8/ui/settings/SettingsRow.tsx +88 -0
  168. package/bizar-dash/src/web/v8/ui/settings/SettingsSection.tsx +109 -0
  169. package/bizar-dash/src/web/v8/ui/styles/globals.css +220 -0
  170. package/bizar-dash/src/web/v8/ui/styles/reset.css +57 -0
  171. package/bizar-dash/src/web/v8/ui/styles/tokens.css +217 -0
  172. package/bizar-dash/src/web/v8/ui/theme/DensityProvider.test.tsx +74 -0
  173. package/bizar-dash/src/web/v8/ui/theme/DensityProvider.tsx +66 -0
  174. package/bizar-dash/src/web/v8/ui/theme/ThemeProvider.test.tsx +86 -0
  175. package/bizar-dash/src/web/v8/ui/theme/ThemeProvider.tsx +94 -0
  176. package/bizar-dash/src/web/v8/ui/theme/ThemeToggle.tsx +88 -0
  177. package/bizar-dash/src/web/v8/ui/theme/useDensity.ts +15 -0
  178. package/bizar-dash/src/web/v8/ui/theme/useTheme.ts +16 -0
  179. package/bizar-dash/src/web/v8/ui/utils/cx.ts +19 -0
  180. package/bizar-dash/src/web/v8/views/Activity/ActivityView.tsx +33 -0
  181. package/bizar-dash/src/web/v8/views/Agents/AgentsView.tsx +108 -0
  182. package/bizar-dash/src/web/v8/views/CommandPalette/AppCommandPalette.test.tsx +142 -0
  183. package/bizar-dash/src/web/v8/views/CommandPalette/AppCommandPalette.tsx +144 -0
  184. package/bizar-dash/src/web/v8/views/Goals/GoalsView.tsx +84 -0
  185. package/bizar-dash/src/web/v8/views/Libraries/LibrariesView.tsx +41 -0
  186. package/bizar-dash/src/web/v8/views/Memory/MemoryView.tsx +61 -0
  187. package/bizar-dash/src/web/v8/views/Overview/OverviewView.tsx +92 -0
  188. package/bizar-dash/src/web/v8/views/Router.tsx +96 -0
  189. package/bizar-dash/src/web/v8/views/Settings/SettingsView.tsx +305 -0
  190. package/bizar-dash/src/web/v8/views/Tasks/CommandPalettePlaceholder.tsx +106 -0
  191. package/bizar-dash/src/web/v8/views/Tasks/TasksKanbanPlaceholder.tsx +62 -0
  192. package/bizar-dash/src/web/v8/views/Tasks/TasksView.tsx +74 -0
  193. package/bizar-dash/src/web/v8/vite-env.d.ts +15 -0
  194. package/bizar-dash/src/web/{mobile.html → v8.html} +4 -7
  195. package/bizar-dash/tests/settings-layout.test.tsx +2 -2
  196. package/bizar-dash/tests/settings-nav.test.tsx +6 -6
  197. package/bizar-dash/tests/setup.ts +26 -0
  198. package/bizar-dash/vite.config.ts +26 -2
  199. package/bizar-dash/vitest.config.ts +5 -3
  200. package/cli/provision-claude.mjs +108 -5
  201. package/package.json +36 -3
  202. package/bizar-dash/dist/assets/EnvVarsSection-BfhAz3ax.js +0 -114
  203. package/bizar-dash/dist/assets/EnvVarsSection-BfhAz3ax.js.map +0 -1
  204. package/bizar-dash/dist/assets/MobileChat-DLWdv6tQ.js +0 -1
  205. package/bizar-dash/dist/assets/MobileChat-DLWdv6tQ.js.map +0 -1
  206. package/bizar-dash/dist/assets/MobileSettings-Bxv62uPY.js +0 -1
  207. package/bizar-dash/dist/assets/MobileSettings-Bxv62uPY.js.map +0 -1
  208. package/bizar-dash/dist/assets/Toast-ATSAmbdi.js +0 -26
  209. package/bizar-dash/dist/assets/Toast-ATSAmbdi.js.map +0 -1
  210. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +0 -1
  211. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +0 -1
  212. package/bizar-dash/dist/assets/main-306CZfn6.js +0 -430
  213. package/bizar-dash/dist/assets/main-306CZfn6.js.map +0 -1
  214. package/bizar-dash/dist/assets/main-G3bNDpY3.css +0 -1
  215. package/bizar-dash/dist/assets/mobile-C_naUN91.js +0 -1
  216. package/bizar-dash/dist/assets/mobile-C_naUN91.js.map +0 -1
  217. package/bizar-dash/dist/assets/mobile-layout-BWLtwtfQ.css +0 -1
  218. package/bizar-dash/dist/assets/mobile-layout-GZ3bE8L6.js +0 -211
  219. package/bizar-dash/dist/assets/mobile-layout-GZ3bE8L6.js.map +0 -1
  220. package/bizar-dash/dist/assets/useSlashCommands-BOwD7lNb.js +0 -65
  221. package/bizar-dash/dist/assets/useSlashCommands-BOwD7lNb.js.map +0 -1
  222. package/bizar-dash/dist/index.html +0 -43
  223. package/bizar-dash/src/web/App.tsx +0 -915
  224. package/bizar-dash/src/web/MobileApp.tsx +0 -414
  225. package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +0 -95
  226. package/bizar-dash/src/web/components/ArtifactListDialog.tsx +0 -55
  227. package/bizar-dash/src/web/components/ArtifactViewer.tsx +0 -144
  228. package/bizar-dash/src/web/components/AuditDialog.tsx +0 -79
  229. package/bizar-dash/src/web/components/AutosaveField.tsx +0 -50
  230. package/bizar-dash/src/web/components/BackupRestore.tsx +0 -330
  231. package/bizar-dash/src/web/components/BgStatusBadge.tsx +0 -32
  232. package/bizar-dash/src/web/components/Button.tsx +0 -56
  233. package/bizar-dash/src/web/components/CanvasContextMenu.tsx +0 -68
  234. package/bizar-dash/src/web/components/Card.tsx +0 -40
  235. package/bizar-dash/src/web/components/CollapsibleSection.tsx +0 -72
  236. package/bizar-dash/src/web/components/CommandDialog.tsx +0 -66
  237. package/bizar-dash/src/web/components/DoctorPanel.tsx +0 -160
  238. package/bizar-dash/src/web/components/EmptyState.tsx +0 -30
  239. package/bizar-dash/src/web/components/EnvVarManager.tsx +0 -430
  240. package/bizar-dash/src/web/components/EvalDiff.tsx +0 -157
  241. package/bizar-dash/src/web/components/EvalExportButton.tsx +0 -27
  242. package/bizar-dash/src/web/components/EvalRunCard.tsx +0 -78
  243. package/bizar-dash/src/web/components/FileBrowser.tsx +0 -633
  244. package/bizar-dash/src/web/components/HeadroomSettings.tsx +0 -418
  245. package/bizar-dash/src/web/components/HeadroomStatus.tsx +0 -158
  246. package/bizar-dash/src/web/components/HelpDialog.tsx +0 -55
  247. package/bizar-dash/src/web/components/InviteDialog.tsx +0 -205
  248. package/bizar-dash/src/web/components/KillConfirmDialog.tsx +0 -88
  249. package/bizar-dash/src/web/components/MobileBottomNav.tsx +0 -38
  250. package/bizar-dash/src/web/components/MobileDrawer.tsx +0 -68
  251. package/bizar-dash/src/web/components/MobileHeader.tsx +0 -50
  252. package/bizar-dash/src/web/components/MobileLayout.tsx +0 -63
  253. package/bizar-dash/src/web/components/Modal.tsx +0 -215
  254. package/bizar-dash/src/web/components/Notifications.tsx +0 -242
  255. package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +0 -72
  256. package/bizar-dash/src/web/components/ScreenshotCapture.tsx +0 -138
  257. package/bizar-dash/src/web/components/ScreenshotOCR.tsx +0 -42
  258. package/bizar-dash/src/web/components/SearchModal.tsx +0 -227
  259. package/bizar-dash/src/web/components/SettingsNav.tsx +0 -112
  260. package/bizar-dash/src/web/components/SettingsSearch.tsx +0 -328
  261. package/bizar-dash/src/web/components/Sidebar.tsx +0 -89
  262. package/bizar-dash/src/web/components/Spinner.tsx +0 -19
  263. package/bizar-dash/src/web/components/StatusBadge.tsx +0 -56
  264. package/bizar-dash/src/web/components/Tag.tsx +0 -41
  265. package/bizar-dash/src/web/components/TailscaleSettings.tsx +0 -161
  266. package/bizar-dash/src/web/components/TaskCard.tsx +0 -69
  267. package/bizar-dash/src/web/components/TaskCreateSheet.tsx +0 -76
  268. package/bizar-dash/src/web/components/Toast.tsx +0 -150
  269. package/bizar-dash/src/web/components/Toggle.tsx +0 -31
  270. package/bizar-dash/src/web/components/Topbar.tsx +0 -410
  271. package/bizar-dash/src/web/components/UsageChart.tsx +0 -250
  272. package/bizar-dash/src/web/components/UsageTable.tsx +0 -90
  273. package/bizar-dash/src/web/components/VirtualList.tsx +0 -53
  274. package/bizar-dash/src/web/components/VisualPlanDialog.tsx +0 -58
  275. package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +0 -211
  276. package/bizar-dash/src/web/components/VoiceRecorder.tsx +0 -105
  277. package/bizar-dash/src/web/components/WorkspaceSelector.tsx +0 -158
  278. package/bizar-dash/src/web/components/agents/CommunicationLog.tsx +0 -126
  279. package/bizar-dash/src/web/components/agents/DependencyGraph.tsx +0 -163
  280. package/bizar-dash/src/web/components/agents/QualityGates.tsx +0 -123
  281. package/bizar-dash/src/web/components/agents/RealTimeEventLog.tsx +0 -127
  282. package/bizar-dash/src/web/components/agents/RoutingDecisions.test.tsx +0 -148
  283. package/bizar-dash/src/web/components/agents/RoutingDecisions.tsx +0 -160
  284. package/bizar-dash/src/web/components/background/AttachButton.tsx +0 -96
  285. package/bizar-dash/src/web/components/background/TmuxAttachCard.tsx +0 -122
  286. package/bizar-dash/src/web/components/chat/AgentChip.tsx +0 -68
  287. package/bizar-dash/src/web/components/chat/AgentNode.tsx +0 -127
  288. package/bizar-dash/src/web/components/chat/AgentTree.tsx +0 -80
  289. package/bizar-dash/src/web/components/chat/ChatBubble.tsx +0 -60
  290. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +0 -72
  291. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +0 -370
  292. package/bizar-dash/src/web/components/chat/ChatRail.tsx +0 -387
  293. package/bizar-dash/src/web/components/chat/ChatThread.tsx +0 -127
  294. package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +0 -50
  295. package/bizar-dash/src/web/components/chat/Composer.tsx +0 -166
  296. package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +0 -54
  297. package/bizar-dash/src/web/components/chat/EmptyState.tsx +0 -21
  298. package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +0 -18
  299. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +0 -38
  300. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +0 -184
  301. package/bizar-dash/src/web/components/chat/JumpToLatest.tsx +0 -58
  302. package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +0 -19
  303. package/bizar-dash/src/web/components/chat/MessageBlock.tsx +0 -260
  304. package/bizar-dash/src/web/components/chat/MessageBubble.tsx +0 -60
  305. package/bizar-dash/src/web/components/chat/SessionList.tsx +0 -163
  306. package/bizar-dash/src/web/components/chat/SessionRowMenu.tsx +0 -206
  307. package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +0 -37
  308. package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +0 -55
  309. package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +0 -36
  310. package/bizar-dash/src/web/components/chat/_legacy.ts +0 -30
  311. package/bizar-dash/src/web/components/chat/index.ts +0 -31
  312. package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +0 -18
  313. package/bizar-dash/src/web/components/chat/useChat.ts +0 -1190
  314. package/bizar-dash/src/web/components/chat/useSlashCommands.ts +0 -47
  315. package/bizar-dash/src/web/components/goals/GoalInput.tsx +0 -86
  316. package/bizar-dash/src/web/components/goals/PlanVisualization.tsx +0 -111
  317. package/bizar-dash/src/web/components/tasks/BacklogPanel.css +0 -109
  318. package/bizar-dash/src/web/components/tasks/BacklogPanel.tsx +0 -209
  319. package/bizar-dash/src/web/hooks/useAutosave.ts +0 -107
  320. package/bizar-dash/src/web/hooks/useI18n.ts +0 -13
  321. package/bizar-dash/src/web/index.html +0 -37
  322. package/bizar-dash/src/web/lib/api.ts +0 -237
  323. package/bizar-dash/src/web/lib/goapPlanner.ts +0 -614
  324. package/bizar-dash/src/web/lib/i18n.ts +0 -25
  325. package/bizar-dash/src/web/lib/logger.ts +0 -30
  326. package/bizar-dash/src/web/lib/markdown.tsx +0 -59
  327. package/bizar-dash/src/web/lib/search.ts +0 -115
  328. package/bizar-dash/src/web/lib/types.ts +0 -894
  329. package/bizar-dash/src/web/lib/utils.ts +0 -114
  330. package/bizar-dash/src/web/lib/ws.ts +0 -177
  331. package/bizar-dash/src/web/locales/en.json +0 -52
  332. package/bizar-dash/src/web/main.tsx +0 -43
  333. package/bizar-dash/src/web/mobile/MobileApp.tsx +0 -309
  334. package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +0 -38
  335. package/bizar-dash/src/web/mobile/MobileChat.tsx +0 -138
  336. package/bizar-dash/src/web/mobile/MobileDoctor.tsx +0 -80
  337. package/bizar-dash/src/web/mobile/MobileEval.tsx +0 -167
  338. package/bizar-dash/src/web/mobile/MobileMemory.tsx +0 -108
  339. package/bizar-dash/src/web/mobile/MobileOverview.tsx +0 -213
  340. package/bizar-dash/src/web/mobile/MobileSettings.tsx +0 -251
  341. package/bizar-dash/src/web/mobile/MobileTasks.tsx +0 -94
  342. package/bizar-dash/src/web/mobile/MobileTopbar.tsx +0 -115
  343. package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +0 -182
  344. package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +0 -64
  345. package/bizar-dash/src/web/mobile/components/MobileModal.tsx +0 -123
  346. package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +0 -273
  347. package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +0 -374
  348. package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +0 -439
  349. package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +0 -143
  350. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +0 -257
  351. package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +0 -175
  352. package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +0 -191
  353. package/bizar-dash/src/web/mobile/views/MobileMods.tsx +0 -149
  354. package/bizar-dash/src/web/mobile/views/MobileMore.tsx +0 -121
  355. package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +0 -137
  356. package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +0 -26
  357. package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +0 -685
  358. package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +0 -172
  359. package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +0 -363
  360. package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +0 -153
  361. package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +0 -564
  362. package/bizar-dash/src/web/mobile/views/QrCodePanel.tsx +0 -69
  363. package/bizar-dash/src/web/mobile.tsx +0 -14
  364. package/bizar-dash/src/web/styles/agents-redesign.css +0 -311
  365. package/bizar-dash/src/web/styles/chat.css +0 -1740
  366. package/bizar-dash/src/web/styles/glyphs.css +0 -960
  367. package/bizar-dash/src/web/styles/main.css +0 -10391
  368. package/bizar-dash/src/web/styles/memory.css +0 -1182
  369. package/bizar-dash/src/web/styles/minimax-usage.css +0 -842
  370. package/bizar-dash/src/web/styles/mobile-chat.css +0 -5
  371. package/bizar-dash/src/web/styles/mobile-layout.css +0 -233
  372. package/bizar-dash/src/web/styles/mobile.css +0 -2181
  373. package/bizar-dash/src/web/styles/settings.css +0 -795
  374. package/bizar-dash/src/web/styles/skills.css +0 -302
  375. package/bizar-dash/src/web/styles/tasks-redesign.css +0 -253
  376. package/bizar-dash/src/web/styles/tasks.css +0 -605
  377. package/bizar-dash/src/web/ui/controls/Button.tsx +0 -77
  378. package/bizar-dash/src/web/ui/controls/Checkbox.tsx +0 -95
  379. package/bizar-dash/src/web/ui/controls/IconButton.tsx +0 -66
  380. package/bizar-dash/src/web/ui/controls/Kbd.tsx +0 -24
  381. package/bizar-dash/src/web/ui/controls/NumberInput.tsx +0 -77
  382. package/bizar-dash/src/web/ui/controls/RadioGroup.tsx +0 -109
  383. package/bizar-dash/src/web/ui/controls/SearchInput.tsx +0 -91
  384. package/bizar-dash/src/web/ui/controls/Select.tsx +0 -92
  385. package/bizar-dash/src/web/ui/controls/Slider.tsx +0 -70
  386. package/bizar-dash/src/web/ui/controls/TextInput.tsx +0 -67
  387. package/bizar-dash/src/web/ui/controls/Textarea.tsx +0 -56
  388. package/bizar-dash/src/web/ui/controls/Toggle.tsx +0 -79
  389. package/bizar-dash/src/web/ui/controls/controls.css +0 -476
  390. package/bizar-dash/src/web/ui/controls/index.ts +0 -35
  391. package/bizar-dash/src/web/ui/data/BarChart.tsx +0 -60
  392. package/bizar-dash/src/web/ui/data/DataTable.tsx +0 -216
  393. package/bizar-dash/src/web/ui/data/EmptyState.tsx +0 -60
  394. package/bizar-dash/src/web/ui/data/ErrorState.tsx +0 -56
  395. package/bizar-dash/src/web/ui/data/KeyValueList.tsx +0 -106
  396. package/bizar-dash/src/web/ui/data/LoadingState.tsx +0 -65
  397. package/bizar-dash/src/web/ui/data/Sparkline.tsx +0 -87
  398. package/bizar-dash/src/web/ui/data/StatTile.tsx +0 -87
  399. package/bizar-dash/src/web/ui/data/data.css +0 -535
  400. package/bizar-dash/src/web/ui/data/index.ts +0 -31
  401. package/bizar-dash/src/web/ui/feedback/Badge.tsx +0 -49
  402. package/bizar-dash/src/web/ui/feedback/Dialog.tsx +0 -209
  403. package/bizar-dash/src/web/ui/feedback/ProgressBar.tsx +0 -69
  404. package/bizar-dash/src/web/ui/feedback/StatusDot.tsx +0 -50
  405. package/bizar-dash/src/web/ui/feedback/Toast.tsx +0 -200
  406. package/bizar-dash/src/web/ui/feedback/Tooltip.tsx +0 -126
  407. package/bizar-dash/src/web/ui/feedback/feedback.css +0 -314
  408. package/bizar-dash/src/web/ui/feedback/index.ts +0 -43
  409. package/bizar-dash/src/web/ui/index.ts +0 -56
  410. package/bizar-dash/src/web/ui/layout/AppShell.tsx +0 -51
  411. package/bizar-dash/src/web/ui/layout/Breadcrumbs.tsx +0 -97
  412. package/bizar-dash/src/web/ui/layout/Panel.tsx +0 -73
  413. package/bizar-dash/src/web/ui/layout/PanelHeader.tsx +0 -39
  414. package/bizar-dash/src/web/ui/layout/Sidebar.tsx +0 -131
  415. package/bizar-dash/src/web/ui/layout/Tabs.tsx +0 -133
  416. package/bizar-dash/src/web/ui/layout/Topbar.tsx +0 -42
  417. package/bizar-dash/src/web/ui/layout/ViewHeader.tsx +0 -48
  418. package/bizar-dash/src/web/ui/layout/index.ts +0 -34
  419. package/bizar-dash/src/web/ui/layout/layout.css +0 -464
  420. package/bizar-dash/src/web/ui/navigation/CommandPalette.tsx +0 -199
  421. package/bizar-dash/src/web/ui/navigation/NavGroup.tsx +0 -95
  422. package/bizar-dash/src/web/ui/navigation/NavLink.tsx +0 -93
  423. package/bizar-dash/src/web/ui/navigation/index.ts +0 -17
  424. package/bizar-dash/src/web/ui/navigation/navigation.css +0 -343
  425. package/bizar-dash/src/web/ui/primitives/Box.tsx +0 -91
  426. package/bizar-dash/src/web/ui/primitives/Grid.tsx +0 -49
  427. package/bizar-dash/src/web/ui/primitives/Inline.tsx +0 -55
  428. package/bizar-dash/src/web/ui/primitives/Separator.tsx +0 -46
  429. package/bizar-dash/src/web/ui/primitives/Stack.tsx +0 -54
  430. package/bizar-dash/src/web/ui/primitives/VisuallyHidden.tsx +0 -26
  431. package/bizar-dash/src/web/ui/primitives/index.ts +0 -23
  432. package/bizar-dash/src/web/ui/primitives/primitives.css +0 -183
  433. package/bizar-dash/src/web/ui/styles/globals.css +0 -86
  434. package/bizar-dash/src/web/ui/styles/reset.css +0 -125
  435. package/bizar-dash/src/web/ui/styles/tokens.css +0 -208
  436. package/bizar-dash/src/web/ui/theme/ThemeProvider.tsx +0 -142
  437. package/bizar-dash/src/web/ui/theme/tokens.ts +0 -131
  438. package/bizar-dash/src/web/ui/theme/useTheme.ts +0 -22
  439. package/bizar-dash/src/web/ui/utils/cx.ts +0 -42
  440. package/bizar-dash/src/web/views/Activity.tsx +0 -1221
  441. package/bizar-dash/src/web/views/Agents.tsx +0 -964
  442. package/bizar-dash/src/web/views/Artifacts.tsx +0 -1494
  443. package/bizar-dash/src/web/views/BackgroundAgents.tsx +0 -822
  444. package/bizar-dash/src/web/views/Chat.tsx +0 -532
  445. package/bizar-dash/src/web/views/Config.tsx +0 -3
  446. package/bizar-dash/src/web/views/Doctor.tsx +0 -317
  447. package/bizar-dash/src/web/views/Eval.tsx +0 -372
  448. package/bizar-dash/src/web/views/EvalReport.tsx +0 -353
  449. package/bizar-dash/src/web/views/GoalPlanner.tsx +0 -158
  450. package/bizar-dash/src/web/views/Harness.tsx +0 -306
  451. package/bizar-dash/src/web/views/History.tsx +0 -353
  452. package/bizar-dash/src/web/views/Memory.tsx +0 -161
  453. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +0 -942
  454. package/bizar-dash/src/web/views/ModView.tsx +0 -207
  455. package/bizar-dash/src/web/views/Mods.tsx +0 -706
  456. package/bizar-dash/src/web/views/Overview.tsx +0 -1364
  457. package/bizar-dash/src/web/views/Providers.tsx +0 -302
  458. package/bizar-dash/src/web/views/Schedules.tsx +0 -902
  459. package/bizar-dash/src/web/views/Settings.tsx +0 -308
  460. package/bizar-dash/src/web/views/Skills.tsx +0 -383
  461. package/bizar-dash/src/web/views/SpawnAgentModal.tsx +0 -221
  462. package/bizar-dash/src/web/views/Tasks.tsx +0 -789
  463. package/bizar-dash/src/web/views/Workspace.tsx +0 -294
  464. package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +0 -844
  465. package/bizar-dash/src/web/views/glyphs/components.tsx +0 -1098
  466. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +0 -426
  467. package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +0 -23
  468. package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +0 -220
  469. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +0 -310
  470. package/bizar-dash/src/web/views/memory/MemoryGraphLegend.tsx +0 -29
  471. package/bizar-dash/src/web/views/memory/MemoryGraphPanel.tsx +0 -192
  472. package/bizar-dash/src/web/views/memory/MemoryGraphView.tsx +0 -336
  473. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +0 -367
  474. package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +0 -160
  475. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +0 -650
  476. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +0 -197
  477. package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +0 -101
  478. package/bizar-dash/src/web/views/settings/ActivitySection.tsx +0 -205
  479. package/bizar-dash/src/web/views/settings/AgentSection.tsx +0 -305
  480. package/bizar-dash/src/web/views/settings/AuthSection.tsx +0 -159
  481. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +0 -15
  482. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +0 -128
  483. package/bizar-dash/src/web/views/settings/HeadroomSection.tsx +0 -39
  484. package/bizar-dash/src/web/views/settings/MemorySection.tsx +0 -154
  485. package/bizar-dash/src/web/views/settings/NetworkSection.tsx +0 -87
  486. package/bizar-dash/src/web/views/settings/NotificationsSection.tsx +0 -34
  487. package/bizar-dash/src/web/views/settings/SystemLlmSection.tsx +0 -81
  488. package/bizar-dash/src/web/views/settings/ThemeSection.tsx +0 -168
  489. package/bizar-dash/src/web/views/settings/UpdatesSection.tsx +0 -256
  490. package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +0 -189
@@ -1,1494 +0,0 @@
1
- // src/views/Artifacts.tsx — v3.1.0 artifact editor with fullscreen canvas + floating controls.
2
- import React from 'react';
3
- import { useEffect, useMemo, useRef, useState } from 'react';
4
- import { GlyphRenderer } from './glyphs/GlyphRenderer';
5
- import {
6
- ArrowLeft,
7
- Map as MapIcon,
8
- Plus,
9
- RefreshCw,
10
- Settings as SettingsIcon,
11
- MessageCircle,
12
- MessageSquare,
13
- Maximize2,
14
- Minimize2,
15
- X,
16
- Trash2,
17
- Pencil,
18
- Link2,
19
- Send,
20
- Check,
21
- Circle,
22
- StopCircle,
23
- AlertCircle,
24
- HelpCircle,
25
- CheckSquare,
26
- StickyNote,
27
- ChevronRight,
28
- Search,
29
- Activity,
30
- Filter,
31
- Archive,
32
- ArchiveRestore,
33
- Copy,
34
- ArrowUp,
35
- ArrowDown,
36
- Layers,
37
- } from 'lucide-react';
38
- import { Button } from '../components/Button';
39
- import { Card, CardTitle, CardMeta } from '../components/Card';
40
- import { EmptyState } from '../components/EmptyState';
41
- import { Spinner } from '../components/Spinner';
42
- import { StatusBadge } from '../components/StatusBadge';
43
- import { useToast } from '../components/Toast';
44
- import { useModal } from '../components/Modal';
45
- import { CanvasContextMenu, type ContextMenuState } from '../components/CanvasContextMenu';
46
- import { api } from '../lib/api';
47
- import { cn, formatRelative, truncate } from '../lib/utils';
48
- import { logger } from '../lib/logger';
49
- import type {
50
- Canvas,
51
- CanvasComment,
52
- CanvasConnection,
53
- CanvasElement,
54
- Artifact,
55
- Settings,
56
- Snapshot,
57
- } from '../lib/types';
58
-
59
- type Props = {
60
- snapshot: Snapshot;
61
- settings: Settings;
62
- activeTab: string;
63
- setActiveTab: (id: string) => void;
64
- refreshSnapshot: () => Promise<void>;
65
- };
66
-
67
- const ELEMENT_TYPES = [
68
- { id: 'task', label: 'Task', color: 'var(--info)', Icon: CheckSquare },
69
- { id: 'note', label: 'Note', color: 'var(--text-dim)', Icon: StickyNote },
70
- { id: 'decision', label: 'Decision', color: 'var(--accent)', Icon: Check },
71
- { id: 'question', label: 'Question', color: 'var(--warning)', Icon: HelpCircle },
72
- ];
73
-
74
- const PLAN_STATUSES = ['draft', 'approved', 'in-progress', 'done', 'rejected', 'archived'];
75
-
76
- function planStatusKind(status: string): 'success' | 'info' | 'error' | 'accent' | 'neutral' | 'warning' {
77
- switch (status) {
78
- case 'approved':
79
- case 'done':
80
- return 'success';
81
- case 'in-progress':
82
- case 'doing':
83
- return 'info';
84
- case 'rejected':
85
- return 'error';
86
- case 'archived':
87
- return 'warning';
88
- case 'draft':
89
- default:
90
- return 'neutral';
91
- }
92
- }
93
-
94
- function elementColor(type: string): string {
95
- return ELEMENT_TYPES.find((t) => t.id === type)?.color || 'var(--text-dim)';
96
- }
97
-
98
- function ElementIcon({ type, size = 12 }: { type: string; size?: number }) {
99
- const def = ELEMENT_TYPES.find((t) => t.id === type) || ELEMENT_TYPES[1];
100
- const Icon = def.Icon;
101
- return <Icon size={size} style={{ color: def.color }} />;
102
- }
103
-
104
- // ── List view ──────────────────────────────────────────────────────────
105
-
106
- export function Artifacts({ snapshot, refreshSnapshot }: Props) {
107
- const toast = useToast();
108
- const [artifacts, setPlans] = useState<Artifact[]>(snapshot.artifacts || []);
109
- const [loading, setLoading] = useState(!snapshot.artifacts);
110
- const [activeSlug, setActiveSlug] = useState<string | null>(null);
111
- const [filter, setFilter] = useState('');
112
- const [showArchived, setShowArchived] = useState(false);
113
-
114
- useEffect(() => {
115
- if (snapshot.artifacts) {
116
- setPlans(snapshot.artifacts);
117
- setLoading(false);
118
- }
119
- }, [snapshot.artifacts]);
120
-
121
- const reload = async () => {
122
- try {
123
- const d = await api.get<{ artifacts: Artifact[] }>('/artifacts');
124
- setPlans(d.artifacts || []);
125
- setLoading(false);
126
- } catch (err) {
127
- toast.error(`Artifacts load failed: ${(err as Error).message}`);
128
- setLoading(false);
129
- }
130
- };
131
-
132
- const filtered = useMemo(() => {
133
- let out = artifacts;
134
- if (filter) {
135
- const q = filter.toLowerCase();
136
- out = out.filter(
137
- (p) =>
138
- (p.slug || '').toLowerCase().includes(q) ||
139
- (p.title || '').toLowerCase().includes(q),
140
- );
141
- }
142
- if (!showArchived) {
143
- out = out.filter((p) => p.status !== 'archived');
144
- }
145
- return out;
146
- }, [artifacts, filter, showArchived]);
147
-
148
- const onCreate = async (slug: string, title?: string) => {
149
- try {
150
- const artifact = await api.post<{ slug: string }>('/artifacts', { slug, title });
151
- toast.success(`Artifact "${artifact.slug}" created.`);
152
- setActiveSlug(artifact.slug);
153
- await reload();
154
- } catch (err) {
155
- toast.error(`Create failed: ${(err as Error).message}`);
156
- }
157
- };
158
-
159
- const onDelete = async (slug: string) => {
160
- if (!confirm(`Delete artifact "${slug}"? This removes the directory permanently.`)) return;
161
- try {
162
- await api.del(`/artifacts/${encodeURIComponent(slug)}`);
163
- toast.success('Artifact deleted.');
164
- if (activeSlug === slug) setActiveSlug(null);
165
- await reload();
166
- } catch (err) {
167
- toast.error(`Delete failed: ${(err as Error).message}`);
168
- }
169
- };
170
-
171
- if (activeSlug) {
172
- // v3.21.0 — Use the new GlyphRenderer (agent-native-style MDX renderer)
173
- // instead of the hand-rolled PlanEditor canvas. Toggle available via the
174
- // "Use MDX" button in the header (TODO: surface this once stable).
175
- return (
176
- <div className="artifact-glyph-overlay">
177
- <div className="artifact-glyph-toolbar">
178
- <Button variant="ghost" onClick={() => { setActiveSlug(null); reload(); }}>
179
- <ArrowLeft size={14} /> Back to artifacts
180
- </Button>
181
- </div>
182
- <GlyphRenderer
183
- slug={activeSlug}
184
- onClose={() => { setActiveSlug(null); reload(); }}
185
- onCommentAdded={() => reload()}
186
- />
187
- </div>
188
- );
189
- }
190
-
191
- return (
192
- <div className="view view-artifacts">
193
- <header className="view-header">
194
- <div className="view-header-text">
195
- <h2 className="view-title">
196
- <MapIcon size={18} /> Glyphs ({artifacts.length})
197
- </h2>
198
- <p className="view-subtitle">
199
- Visual artifacts with elements, connections, and threaded comments.
200
- </p>
201
- </div>
202
- <div className="view-actions">
203
- <div className="search-input">
204
- <Search size={14} aria-hidden />
205
- <label htmlFor="artifacts-search" className="sr-only">Search artifacts</label>
206
- <input
207
- id="artifacts-search"
208
- className="input"
209
- type="text"
210
- placeholder="Search…"
211
- value={filter}
212
- onChange={(e) => setFilter(e.target.value)}
213
- aria-label="Search artifacts"
214
- />
215
- </div>
216
- <Button
217
- variant="ghost"
218
- size="sm"
219
- onClick={() => setShowArchived((v) => !v)}
220
- title={showArchived ? 'Hide archived' : 'Show archived'}
221
- >
222
- {showArchived ? <ArchiveRestore size={14} /> : <Archive size={14} />}
223
- {showArchived ? 'Hide archived' : 'Show archived'}
224
- </Button>
225
- <Button variant="secondary" size="sm" onClick={reload}>
226
- <RefreshCw size={14} /> Refresh
227
- </Button>
228
- </div>
229
- </header>
230
-
231
- <NewPlanCard onCreate={onCreate} />
232
-
233
- {loading ? (
234
- <div className="view-loading">
235
- <Spinner size="lg" />
236
- </div>
237
- ) : filtered.length === 0 ? (
238
- <EmptyState
239
- icon={<MapIcon size={32} />}
240
- title={showArchived ? 'No artifacts' : 'No artifacts yet'}
241
- message={
242
- showArchived
243
- ? 'No artifacts match your filter (try Show archived off).'
244
- : 'Create one above to get started.'
245
- }
246
- />
247
- ) : (
248
- <div className="artifacts-grid">
249
- {filtered.map((p) => (
250
- <PlanCard
251
- key={p.slug}
252
- artifact={p}
253
- onOpen={() => setActiveSlug(p.slug)}
254
- onDelete={() => onDelete(p.slug)}
255
- />
256
- ))}
257
- </div>
258
- )}
259
- </div>
260
- );
261
- }
262
-
263
- function NewPlanCard({ onCreate }: { onCreate: (slug: string, title?: string) => void }) {
264
- const [slug, setSlug] = useState('');
265
- const [title, setTitle] = useState('');
266
- return (
267
- <Card className="new-artifact">
268
- <CardTitle>
269
- <Plus size={14} /> New artifact
270
- </CardTitle>
271
- <CardMeta>Slug must be lowercase, may contain hyphens, 1–64 chars.</CardMeta>
272
- <form
273
- className="new-artifact-form"
274
- onSubmit={(e) => {
275
- e.preventDefault();
276
- if (!slug.trim()) return;
277
- onCreate(slug.trim(), title.trim() || undefined);
278
- setSlug('');
279
- setTitle('');
280
- }}
281
- >
282
- <label htmlFor="new-artifact-slug" className="sr-only">Artifact slug</label>
283
- <input
284
- id="new-artifact-slug"
285
- className="input"
286
- type="text"
287
- placeholder="slug (e.g. dashboard-v3.1)"
288
- pattern="[a-z0-9][a-z0-9-]{0,63}"
289
- required
290
- value={slug}
291
- onChange={(e) => setSlug(e.target.value)}
292
- aria-label="Artifact slug"
293
- />
294
- <label htmlFor="new-artifact-title" className="sr-only">Artifact title</label>
295
- <input
296
- id="new-artifact-title"
297
- className="input"
298
- type="text"
299
- placeholder="Title (optional)"
300
- value={title}
301
- onChange={(e) => setTitle(e.target.value)}
302
- />
303
- <Button variant="primary" type="submit">
304
- Create
305
- </Button>
306
- </form>
307
- </Card>
308
- );
309
- }
310
-
311
- function PlanCard({
312
- artifact,
313
- onOpen,
314
- onDelete,
315
- }: {
316
- artifact: Artifact;
317
- onOpen: () => void;
318
- onDelete: () => void;
319
- }) {
320
- const kind = planStatusKind(artifact.status || 'draft');
321
- return (
322
- <Card
323
- variant="elevated"
324
- interactive
325
- className="artifact-card"
326
- onClick={onOpen}
327
- >
328
- <div className="artifact-card-head">
329
- <div className="artifact-card-title">{artifact.title || artifact.slug}</div>
330
- <StatusBadge kind={kind}>{artifact.status || 'draft'}</StatusBadge>
331
- </div>
332
- <div className="artifact-card-slug mono">{artifact.slug} · {artifact.source}</div>
333
- <div className="artifact-card-meta">
334
- {artifact.elementCount != null && <span>{artifact.elementCount} elements</span>}
335
- {artifact.commentCount != null && <span> · {artifact.commentCount} comments</span>}
336
- <span> · edited {formatRelative(artifact.mtime)}</span>
337
- </div>
338
- <div className="artifact-card-actions">
339
- <Button variant="primary" size="sm" onClick={(e) => { e.stopPropagation(); onOpen(); }}>
340
- Open
341
- <ChevronRight size={12} />
342
- </Button>
343
- <Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); onDelete(); }}>
344
- <Trash2 size={12} /> Delete
345
- </Button>
346
- </div>
347
- </Card>
348
- );
349
- }
350
-
351
- // ── Artifact editor (fullscreen) ──────────────────────────────────────────
352
-
353
- function PlanEditor({
354
- slug,
355
- onBack,
356
- onDelete,
357
- }: {
358
- slug: string;
359
- onBack: () => void;
360
- onDelete: () => void;
361
- }) {
362
- const toast = useToast();
363
- const modal = useModal();
364
- const [canvas, setCanvas] = useState<Canvas | null>(null);
365
- const [meta, setMeta] = useState<{ title: string; status: string; tags: string[]; description?: string } | null>(null);
366
- const [loading, setLoading] = useState(true);
367
- const [selectedElId, setSelectedElId] = useState<string | null>(null);
368
- const [fullscreen, setFullscreen] = useState(false);
369
- const [showComments, setShowComments] = useState(true);
370
- const [contextMenu, setContextMenu] = useState<ContextMenuState>(null);
371
- const [contextMenuWorldPos, setContextMenuWorldPos] = useState<{ x: number; y: number } | null>(null);
372
- // v3.7.0 — Element-specific right-click context menu
373
- const [elementContextMenu, setElementContextMenu] = useState<ContextMenuState>(null);
374
-
375
- const moveElementLocally = (id: string, x: number, y: number) => {
376
- setCanvas((cur) => {
377
- if (!cur) return cur;
378
- return {
379
- ...cur,
380
- elements: cur.elements.map((el) => (el.id === id ? { ...el, x, y } : el)),
381
- };
382
- });
383
- };
384
-
385
- const reload = async () => {
386
- setLoading(true);
387
- try {
388
- const artifact = await api.get<{ meta: { title: string; status: string; tags: string[]; description?: string }; canvas: Canvas }>(
389
- `/artifacts/${encodeURIComponent(slug)}`,
390
- );
391
- setCanvas(artifact.canvas);
392
- setMeta(artifact.meta);
393
- } catch (err) {
394
- toast.error(`Could not load artifact: ${(err as Error).message}`);
395
- } finally {
396
- setLoading(false);
397
- }
398
- };
399
-
400
- useEffect(() => {
401
- setSelectedElId(null);
402
- reload();
403
- // eslint-disable-next-line react-hooks/exhaustive-deps
404
- }, [slug]);
405
-
406
- // Issue 3 fix — wire fullscreen state to browser Fullscreen API
407
- useEffect(() => {
408
- if (!fullscreen) {
409
- if (document.fullscreenElement) {
410
- document.exitFullscreen().catch(() => {});
411
- }
412
- return;
413
- }
414
- // Sync state when user exits via ESC key — before entering, so we don't
415
- // double-set on the way in
416
- const onFullscreenChange = () => {
417
- if (!document.fullscreenElement) setFullscreen(false);
418
- };
419
- document.addEventListener('fullscreenchange', onFullscreenChange);
420
- // Attempt to enter fullscreen (fails silently if unsupported or blocked)
421
- document.documentElement.requestFullscreen().catch(() => {});
422
- return () => {
423
- document.removeEventListener('fullscreenchange', onFullscreenChange);
424
- if (document.fullscreenElement) {
425
- document.exitFullscreen().catch(() => {});
426
- }
427
- };
428
- // eslint-disable-next-line react-hooks/exhaustive-deps
429
- }, [fullscreen]);
430
-
431
- const totalComments = canvas?.comments.length ?? 0;
432
-
433
- const addElement = async (body: { type: string; title: string; content: string; x: number; y: number }, position?: { x: number; y: number }) => {
434
- try {
435
- const payload = position ? { ...body, x: position.x, y: position.y } : body;
436
- await api.post(`/artifacts/${encodeURIComponent(slug)}/elements`, payload);
437
- await reload();
438
- toast.success('Element added.');
439
- } catch (err) {
440
- toast.error(`Add element failed: ${(err as Error).message}`);
441
- }
442
- };
443
-
444
- const updateElement = async (id: string, body: Partial<CanvasElement>) => {
445
- try {
446
- await api.put(`/artifacts/${encodeURIComponent(slug)}/elements/${encodeURIComponent(id)}`, body);
447
- await reload();
448
- } catch (err) {
449
- toast.error(`Update failed: ${(err as Error).message}`);
450
- }
451
- };
452
-
453
- const deleteElement = async (id: string) => {
454
- try {
455
- await api.del(`/artifacts/${encodeURIComponent(slug)}/elements/${encodeURIComponent(id)}`);
456
- if (selectedElId === id) setSelectedElId(null);
457
- await reload();
458
- toast.success('Element deleted.');
459
- } catch (err) {
460
- toast.error(`Delete failed: ${(err as Error).message}`);
461
- }
462
- };
463
-
464
- // v3.7.0 — Duplicate an element with a small position offset
465
- const duplicateElement = async (id: string) => {
466
- const el = canvas?.elements.find((e) => e.id === id);
467
- if (!el) return;
468
- try {
469
- await api.post(`/artifacts/${encodeURIComponent(slug)}/elements`, {
470
- type: el.type,
471
- title: el.title ? `${el.title} (copy)` : 'Untitled',
472
- content: el.content || '',
473
- x: (el.x || 0) + 24,
474
- y: (el.y || 0) + 24,
475
- });
476
- await reload();
477
- toast.success('Element duplicated.');
478
- } catch (err) {
479
- toast.error(`Duplicate failed: ${(err as Error).message}`);
480
- }
481
- };
482
-
483
- // v3.7.0 — Change element type
484
- const changeElementType = async (id: string, newType: string) => {
485
- try {
486
- await api.put(`/artifacts/${encodeURIComponent(slug)}/elements/${encodeURIComponent(id)}`, { type: newType });
487
- await reload();
488
- toast.success(`Type changed to ${newType}.`);
489
- } catch (err) {
490
- toast.error(`Change type failed: ${(err as Error).message}`);
491
- }
492
- };
493
-
494
- const addConnection = async (from: string, to: string, label?: string) => {
495
- try {
496
- await api.post(`/artifacts/${encodeURIComponent(slug)}/connections`, { from, to, label });
497
- await reload();
498
- toast.success('Connection added.');
499
- } catch (err) {
500
- toast.error(`Connection failed: ${(err as Error).message}`);
501
- }
502
- };
503
-
504
- const deleteConnection = async (id: string) => {
505
- try {
506
- await api.del(`/artifacts/${encodeURIComponent(slug)}/connections/${encodeURIComponent(id)}`);
507
- await reload();
508
- } catch (err) {
509
- toast.error(`Delete failed: ${(err as Error).message}`);
510
- }
511
- };
512
-
513
- const addComment = async (text: string, elementId: string | null, worldPos?: { x: number; y: number }) => {
514
- try {
515
- const path = elementId
516
- ? `/artifacts/${encodeURIComponent(slug)}/elements/${encodeURIComponent(elementId)}/comments`
517
- : `/artifacts/${encodeURIComponent(slug)}/comments`;
518
- const body: { text: string; elementId: string | null; x?: number; y?: number } = { text, elementId };
519
- if (worldPos) { body.x = worldPos.x; body.y = worldPos.y; }
520
- await api.post(path, body);
521
- await reload();
522
- toast.success('Comment added.');
523
- } catch (err) {
524
- toast.error(`Comment failed: ${(err as Error).message}`);
525
- }
526
- };
527
-
528
- const deleteComment = async (cid: string) => {
529
- try {
530
- await api.del(`/artifacts/${encodeURIComponent(slug)}/comments/${encodeURIComponent(cid)}`);
531
- await reload();
532
- } catch (err) {
533
- toast.error(`Delete failed: ${(err as Error).message}`);
534
- }
535
- };
536
-
537
- const updatePositions = async (positions: { id: string; x: number; y: number }[]) => {
538
- try {
539
- await api.put(`/artifacts/${encodeURIComponent(slug)}/position`, { positions });
540
- } catch (err) {
541
- // non-fatal
542
- logger.warn('Position update failed', err);
543
- }
544
- };
545
-
546
- const onConfigure = () => {
547
- if (!meta) return;
548
- let titleEl: HTMLInputElement | null = null;
549
- let descEl: HTMLTextAreaElement | null = null;
550
- let tagsEl: HTMLInputElement | null = null;
551
- let statusEl: HTMLSelectElement | null = null;
552
- modal.open({
553
- title: 'Configure artifact',
554
- width: 560,
555
- children: (
556
- <div className="artifact-config-form">
557
- <label className="field-label" htmlFor="artifact-cfg-title">Title</label>
558
- <input id="artifact-cfg-title" ref={(el) => { titleEl = el; }} className="input" type="text" defaultValue={meta.title} />
559
- <label className="field-label" htmlFor="artifact-cfg-desc">Description (markdown)</label>
560
- <textarea id="artifact-cfg-desc" ref={(el) => { descEl = el; }} className="textarea" rows={3} defaultValue={meta.description || ''} />
561
- <label className="field-label" htmlFor="artifact-cfg-tags">Tags (comma-separated)</label>
562
- <input id="artifact-cfg-tags" ref={(el) => { tagsEl = el; }} className="input" type="text" defaultValue={(meta.tags || []).join(', ')} />
563
- <label className="field-label" htmlFor="artifact-cfg-status">Status</label>
564
- <select id="artifact-cfg-status" ref={(el) => { statusEl = el; }} className="select" defaultValue={meta.status}>
565
- {PLAN_STATUSES.map((s) => (
566
- <option key={s} value={s}>{s}</option>
567
- ))}
568
- </select>
569
- </div>
570
- ),
571
- footer: (
572
- <div className="modal-footer-actions">
573
- <Button variant="ghost" onClick={() => modal.close()}>Cancel</Button>
574
- <Button
575
- variant="primary"
576
- onClick={async () => {
577
- try {
578
- const tags = (tagsEl?.value || '').split(',').map((t) => t.trim()).filter(Boolean);
579
- await api.put(`/artifacts/${encodeURIComponent(slug)}`, {
580
- title: (titleEl?.value || '').trim() || meta.title,
581
- description: descEl?.value || '',
582
- tags,
583
- status: statusEl?.value || meta.status,
584
- });
585
- await reload();
586
- toast.success('Artifact updated.');
587
- modal.close();
588
- } catch (err) {
589
- toast.error(`Save failed: ${(err as Error).message}`);
590
- }
591
- }}
592
- >
593
- Save
594
- </Button>
595
- </div>
596
- ),
597
- });
598
- };
599
-
600
- const onAddElement = (worldPos?: { x: number; y: number }) => {
601
- let typeEl: HTMLSelectElement | null = null;
602
- let titleEl: HTMLInputElement | null = null;
603
- let contentEl: HTMLTextAreaElement | null = null;
604
- modal.open({
605
- title: 'Add element',
606
- width: 520,
607
- children: (
608
- <div className="artifact-element-form">
609
- <label className="field-label" htmlFor="artifact-elem-type">Type</label>
610
- <select id="artifact-elem-type" ref={(el) => { typeEl = el; }} className="select" defaultValue="task">
611
- {ELEMENT_TYPES.map((t) => (
612
- <option key={t.id} value={t.id}>{t.label}</option>
613
- ))}
614
- </select>
615
- <label className="field-label" htmlFor="artifact-elem-title">Title</label>
616
- <input id="artifact-elem-title" ref={(el) => { titleEl = el; }} className="input" type="text" placeholder="Element title" autoFocus />
617
- <label className="field-label" htmlFor="artifact-elem-content">Content (markdown)</label>
618
- <textarea id="artifact-elem-content" ref={(el) => { contentEl = el; }} className="textarea" rows={4} placeholder="Description…" />
619
- </div>
620
- ),
621
- footer: (
622
- <div className="modal-footer-actions">
623
- <Button variant="ghost" onClick={() => modal.close()}>Cancel</Button>
624
- <Button
625
- variant="primary"
626
- onClick={async (e) => {
627
- e.preventDefault();
628
- e.stopPropagation();
629
- if (worldPos) {
630
- await addElement({
631
- type: typeEl?.value || 'note',
632
- title: (titleEl?.value || 'Untitled').trim(),
633
- content: contentEl?.value || '',
634
- x: worldPos.x,
635
- y: worldPos.y,
636
- });
637
- } else {
638
- const maxX = (canvas?.elements || []).reduce((acc, el) => Math.max(acc, el.x || 0), 0);
639
- const maxY = (canvas?.elements || []).reduce((acc, el) => Math.max(acc, el.y || 0), 0);
640
- await addElement({
641
- type: typeEl?.value || 'note',
642
- title: (titleEl?.value || 'Untitled').trim(),
643
- content: contentEl?.value || '',
644
- x: 80 + Math.min(maxX + 40, 200),
645
- y: 80 + Math.min(maxY + 40, 200),
646
- });
647
- }
648
- modal.close();
649
- }}
650
- >
651
- <Plus size={12} /> Add
652
- </Button>
653
- </div>
654
- ),
655
- });
656
- };
657
-
658
- const onCanvasComment = (worldPos?: { x: number; y: number }) => {
659
- let textEl: HTMLTextAreaElement | null = null;
660
- modal.open({
661
- title: 'Add canvas comment',
662
- width: 480,
663
- children: (
664
- <div>
665
- <label className="field-label" htmlFor="canvas-comment">Comment</label>
666
- <textarea id="canvas-comment" ref={(el) => { textEl = el; }} className="textarea" rows={4} placeholder="Comment for the whole artifact…" autoFocus />
667
- </div>
668
- ),
669
- footer: (
670
- <div className="modal-footer-actions">
671
- <Button variant="ghost" onClick={() => modal.close()}>Cancel</Button>
672
- <Button
673
- variant="primary"
674
- onClick={async (e) => {
675
- e.preventDefault();
676
- e.stopPropagation();
677
- const text = (textEl?.value || '').trim();
678
- if (!text) return;
679
- await addComment(text, null, worldPos);
680
- modal.close();
681
- }}
682
- >
683
- <Send size={12} /> Post
684
- </Button>
685
- </div>
686
- ),
687
- });
688
- };
689
-
690
- if (loading || !canvas || !meta) {
691
- return (
692
- <div className="view view-artifacts view-artifacts-fullscreen">
693
- <PlanEditorHeader
694
- slug={slug}
695
- meta={meta || { title: slug, status: 'draft', tags: [] }}
696
- counts={{ elements: 0, comments: 0 }}
697
- onBack={onBack}
698
- onConfigure={() => undefined}
699
- onAddElement={() => undefined}
700
- onCanvasComment={() => undefined}
701
- onRefresh={reload}
702
- onDelete={onDelete}
703
- fullscreen={fullscreen}
704
- setFullscreen={setFullscreen}
705
- />
706
- <div className="view-loading" style={{ flex: 1 }}>
707
- <Spinner size="lg" />
708
- <p>Loading canvas…</p>
709
- </div>
710
- </div>
711
- );
712
- }
713
-
714
- const visibleComments = selectedElId
715
- ? canvas.comments.filter((c) => c.elementId === selectedElId)
716
- : canvas.comments;
717
-
718
- return (
719
- <div className={cn('view view-artifacts view-artifacts-fullscreen', !fullscreen && 'view-artifacts-embedded')}>
720
- {!fullscreen && (
721
- <PlanEditorHeader
722
- slug={slug}
723
- meta={meta}
724
- counts={{ elements: canvas.elements.length, comments: totalComments }}
725
- onBack={onBack}
726
- onConfigure={onConfigure}
727
- onAddElement={onAddElement}
728
- onCanvasComment={onCanvasComment}
729
- onRefresh={reload}
730
- onDelete={onDelete}
731
- fullscreen={fullscreen}
732
- setFullscreen={setFullscreen}
733
- />
734
- )}
735
-
736
- <div className="artifacts-body">
737
- {/* Canvas wrapper with floating controls */}
738
- <div className="artifact-canvas-wrapper">
739
- {fullscreen && (
740
- <div className="artifact-canvas-floating-controls">
741
- <Button variant="ghost" size="sm" onClick={onBack} title="Back to artifacts list">
742
- <ArrowLeft size={14} /> Back
743
- </Button>
744
- <h3>{meta?.title || slug}</h3>
745
- {meta && <StatusBadge kind={planStatusKind(meta.status)}>{meta.status}</StatusBadge>}
746
- <Button variant="secondary" size="sm" onClick={() => onAddElement()}>
747
- <Plus size={14} /> Element
748
- </Button>
749
- <Button variant="secondary" size="sm" onClick={() => onCanvasComment()}>
750
- <MessageCircle size={14} /> Comment
751
- </Button>
752
- <Button variant="ghost" size="sm" onClick={onConfigure} title="Configure artifact" aria-label="Configure artifact">
753
- <SettingsIcon size={14} />
754
- </Button>
755
- </div>
756
- )}
757
-
758
- <CanvasViewport
759
- canvas={canvas}
760
- selectedElId={selectedElId}
761
- onSelect={setSelectedElId}
762
- onMoveElement={moveElementLocally}
763
- onMoveEnd={updatePositions}
764
- onDeleteElement={deleteElement}
765
- onConnect={(from, to) => addConnection(from, to)}
766
- onDeleteConnection={deleteConnection}
767
- onEditElement={(el) => editElementInline(modal, slug, el, updateElement, toast)}
768
- onContextMenu={(e, worldPos) => {
769
- e.preventDefault();
770
- setElementContextMenu(null);
771
- setContextMenuWorldPos(worldPos);
772
- setContextMenu({
773
- x: e.clientX,
774
- y: e.clientY,
775
- items: [
776
- { label: 'Add element', icon: Plus, onClick: () => onAddElement(worldPos) },
777
- { label: 'Add comment', icon: MessageSquare, onClick: () => onCanvasComment(worldPos) },
778
- { type: 'separator' },
779
- { label: 'Configure artifact', icon: SettingsIcon, onClick: onConfigure },
780
- { label: 'Delete artifact', icon: Trash2, onClick: onDelete },
781
- ],
782
- });
783
- }}
784
- onElementContextMenu={(e, elementId, _worldPos) => {
785
- const el = canvas?.elements.find((x) => x.id === elementId);
786
- if (!el) return;
787
- setContextMenu(null);
788
- setElementContextMenu({
789
- x: e.clientX,
790
- y: e.clientY,
791
- items: [
792
- { label: 'Edit', icon: Pencil, onClick: () => { editElementInline(modal, slug, el, updateElement, toast); } },
793
- { label: 'Duplicate', icon: Copy, onClick: () => duplicateElement(elementId) },
794
- { type: 'separator' },
795
- { label: 'Change type', icon: Layers, onClick: () => {
796
- // Cycle through types: task -> note -> decision -> question -> task
797
- const types = ['task', 'note', 'decision', 'question'];
798
- const currentIdx = types.indexOf(el.type);
799
- const nextType = types[(currentIdx + 1) % types.length];
800
- changeElementType(elementId, nextType);
801
- }},
802
- { type: 'separator' },
803
- { label: `Delete`, icon: Trash2, onClick: () => { if (confirm(`Delete "${el.title || el.type}"?`)) deleteElement(elementId); } },
804
- ],
805
- });
806
- }}
807
- />
808
-
809
- {/* Canvas-level context menu */}
810
- <CanvasContextMenu menu={contextMenu} onClose={() => setContextMenu(null)} />
811
- {/* Element-level context menu */}
812
- <CanvasContextMenu menu={elementContextMenu} onClose={() => setElementContextMenu(null)} />
813
- </div>
814
-
815
- {showComments && (
816
- <CommentsPanel
817
- slug={slug}
818
- comments={visibleComments}
819
- selectedElId={selectedElId}
820
- onAdd={(text) => addComment(text, selectedElId)}
821
- onDelete={deleteComment}
822
- onClose={() => {
823
- setShowComments(false);
824
- setSelectedElId(null);
825
- }}
826
- />
827
- )}
828
- </div>
829
- </div>
830
- );
831
- }
832
-
833
- function PlanEditorHeader({
834
- slug,
835
- meta,
836
- counts,
837
- onBack,
838
- onConfigure,
839
- onAddElement,
840
- onCanvasComment,
841
- onRefresh,
842
- onDelete,
843
- fullscreen,
844
- setFullscreen,
845
- }: {
846
- slug: string;
847
- meta: { title: string; status: string; tags: string[] };
848
- counts: { elements: number; comments: number };
849
- onBack: () => void;
850
- onConfigure: () => void;
851
- onAddElement: () => void;
852
- onCanvasComment: () => void;
853
- onRefresh: () => void;
854
- onDelete: () => void;
855
- fullscreen: boolean;
856
- setFullscreen: (v: boolean) => void;
857
- }) {
858
- const kind = planStatusKind(meta.status);
859
- return (
860
- <header className="artifacts-editor-bar">
861
- <div className="artifacts-editor-bar-left">
862
- <Button variant="ghost" size="sm" onClick={onBack} title="Back to artifacts list">
863
- <ArrowLeft size={14} /> Back
864
- </Button>
865
- <div className="artifacts-editor-meta">
866
- <span className="artifacts-editor-title">{meta.title || slug}</span>
867
- <span className="artifacts-editor-slug mono">{slug}</span>
868
- <StatusBadge kind={kind}>{meta.status}</StatusBadge>
869
- </div>
870
- </div>
871
- <div className="artifacts-editor-bar-right">
872
- <span className="muted text-sm">
873
- {counts.elements} element{counts.elements === 1 ? '' : 's'} · {counts.comments} comment{counts.comments === 1 ? '' : 's'}
874
- </span>
875
- <Button variant="secondary" size="sm" onClick={onAddElement}>
876
- <Plus size={14} /> Element
877
- </Button>
878
- <Button variant="secondary" size="sm" onClick={onCanvasComment}>
879
- <MessageCircle size={14} /> Comment
880
- </Button>
881
- <Button variant="ghost" size="sm" onClick={onConfigure} title="Configure artifact" aria-label="Configure artifact">
882
- <SettingsIcon size={14} />
883
- </Button>
884
- <Button variant="ghost" size="sm" onClick={onRefresh} title="Refresh canvas" aria-label="Refresh canvas">
885
- <RefreshCw size={14} />
886
- </Button>
887
- <Button
888
- variant="ghost"
889
- size="sm"
890
- onClick={() => setFullscreen(!fullscreen)}
891
- title={fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
892
- aria-label={fullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
893
- >
894
- {fullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
895
- </Button>
896
- <Button variant="ghost" size="sm" onClick={onDelete} title="Delete artifact" aria-label="Delete artifact">
897
- <Trash2 size={14} />
898
- </Button>
899
- </div>
900
- </header>
901
- );
902
- }
903
-
904
- function editElementInline(
905
- modal: ReturnType<typeof useModal>,
906
- slug: string,
907
- el: CanvasElement,
908
- updateElement: (id: string, body: Partial<CanvasElement>) => Promise<void>,
909
- toast: ReturnType<typeof useToast>,
910
- ) {
911
- let typeEl: HTMLSelectElement | null = null;
912
- let titleEl: HTMLInputElement | null = null;
913
- let contentEl: HTMLTextAreaElement | null = null;
914
- let statusEl: HTMLInputElement | null = null;
915
- modal.open({
916
- title: `Edit ${el.title || el.type}`,
917
- width: 520,
918
- children: (
919
- <div className="artifact-element-form">
920
- <label className="field-label" htmlFor="elem-edit-type">Type</label>
921
- <select id="elem-edit-type" ref={(el2) => { typeEl = el2; }} className="select" defaultValue={el.type}>
922
- {ELEMENT_TYPES.map((t) => (
923
- <option key={t.id} value={t.id}>{t.label}</option>
924
- ))}
925
- </select>
926
- <label className="field-label" htmlFor="elem-edit-title">Title</label>
927
- <input id="elem-edit-title" ref={(el2) => { titleEl = el2; }} className="input" type="text" defaultValue={el.title} />
928
- <label className="field-label" htmlFor="elem-edit-status">Status</label>
929
- <input id="elem-edit-status" ref={(el2) => { statusEl = el2; }} className="input" type="text" defaultValue={el.status} placeholder="open / done / blocked …" />
930
- <label className="field-label" htmlFor="elem-edit-content">Content (markdown)</label>
931
- <textarea id="elem-edit-content" ref={(el2) => { contentEl = el2; }} className="textarea" rows={5} defaultValue={el.content} />
932
- </div>
933
- ),
934
- footer: (
935
- <div className="modal-footer-actions">
936
- <Button variant="ghost" onClick={() => modal.close()}>Cancel</Button>
937
- <Button
938
- variant="primary"
939
- onClick={async () => {
940
- try {
941
- await updateElement(el.id, {
942
- type: typeEl?.value || el.type,
943
- title: titleEl?.value ?? el.title,
944
- content: contentEl?.value ?? el.content,
945
- status: statusEl?.value || el.status,
946
- });
947
- toast.success('Element saved.');
948
- modal.close();
949
- } catch (err) {
950
- toast.error(`Save failed: ${(err as Error).message}`);
951
- }
952
- }}
953
- >
954
- Save
955
- </Button>
956
- </div>
957
- ),
958
- });
959
- }
960
-
961
- // ── Canvas viewport ───────────────────────────────────────────────────
962
-
963
- type ViewportProps = {
964
- canvas: Canvas;
965
- selectedElId: string | null;
966
- onSelect: (id: string | null) => void;
967
- onMoveElement: (id: string, x: number, y: number) => void;
968
- onMoveEnd: (positions: { id: string; x: number; y: number }[]) => void;
969
- onDeleteElement: (id: string) => void;
970
- onConnect: (from: string, to: string) => void;
971
- onDeleteConnection: (id: string) => void;
972
- onEditElement: (el: CanvasElement) => void;
973
- onContextMenu?: (e: React.MouseEvent, worldPos: { x: number; y: number }) => void;
974
- // v3.7.0 — Element-specific context menu (right-click on an element)
975
- onElementContextMenu?: (e: React.MouseEvent, elementId: string, worldPos: { x: number; y: number }) => void;
976
- fitToView?: () => void;
977
- };
978
-
979
- function CanvasViewport({
980
- canvas,
981
- selectedElId,
982
- onSelect,
983
- onMoveElement,
984
- onMoveEnd,
985
- onDeleteElement,
986
- onConnect,
987
- onDeleteConnection,
988
- onEditElement,
989
- onContextMenu,
990
- onElementContextMenu,
991
- }: ViewportProps) {
992
- const rootRef = useRef<HTMLDivElement>(null);
993
- const innerRef = useRef<HTMLDivElement>(null);
994
- const elementsRef = useRef(canvas.elements);
995
- const stateRef = useRef({
996
- panning: false,
997
- startX: 0,
998
- startY: 0,
999
- ox: canvas.viewport.x || 0,
1000
- oy: canvas.viewport.y || 0,
1001
- scale: canvas.viewport.zoom || 1,
1002
- dragging: null as null | { id: string; offsetX: number; offsetY: number; moved: boolean; origX: number; origY: number },
1003
- connecting: null as null | { fromId: string; x: number; y: number },
1004
- });
1005
-
1006
- useEffect(() => {
1007
- elementsRef.current = canvas.elements;
1008
- }, [canvas.elements]);
1009
-
1010
- useEffect(() => {
1011
- const root = rootRef.current;
1012
- const inner = innerRef.current;
1013
- if (!root || !inner) return undefined;
1014
- const apply = () => {
1015
- const s = stateRef.current;
1016
- inner.style.transform = `translate(${s.ox}px, ${s.oy}px) scale(${s.scale})`;
1017
- };
1018
- apply();
1019
-
1020
- const screenToWorld = (clientX: number, clientY: number) => {
1021
- const rect = inner.getBoundingClientRect();
1022
- const s = stateRef.current;
1023
- // rect.left/rect.top is the *post-transform* top-left; account for scale
1024
- const x = (clientX - rect.left) / s.scale;
1025
- const y = (clientY - rect.top) / s.scale;
1026
- return { x, y };
1027
- };
1028
-
1029
- const onMouseDown = (e: MouseEvent) => {
1030
- if (e.button === 2) return;
1031
- const t = e.target as HTMLElement;
1032
- if (
1033
- t === root ||
1034
- t.classList.contains('canvas-grid-bg') ||
1035
- t.classList.contains('canvas-inner')
1036
- ) {
1037
- stateRef.current.panning = true;
1038
- stateRef.current.startX = e.clientX - stateRef.current.ox;
1039
- stateRef.current.startY = e.clientY - stateRef.current.oy;
1040
- root.style.cursor = 'grabbing';
1041
- return;
1042
- }
1043
- const elCard = t.closest('.canvas-element') as HTMLElement | null;
1044
- if (elCard) {
1045
- const id = elCard.dataset.elementId;
1046
- if (!id) return;
1047
- const headerHit = !!t.closest('.canvas-element-head');
1048
- const handleHit = !!t.closest('.canvas-element-handle');
1049
- if (headerHit || handleHit) {
1050
- const world = screenToWorld(e.clientX, e.clientY);
1051
- const el = elementsRef.current.find((x) => x.id === id);
1052
- if (!el) return;
1053
- stateRef.current.dragging = {
1054
- id,
1055
- offsetX: world.x - (el.x || 0),
1056
- offsetY: world.y - (el.y || 0),
1057
- moved: false,
1058
- origX: el.x || 0,
1059
- origY: el.y || 0,
1060
- };
1061
- onSelect(id);
1062
- e.preventDefault();
1063
- }
1064
- }
1065
- };
1066
- const onMouseMove = (e: MouseEvent) => {
1067
- const s = stateRef.current;
1068
- if (s.panning) {
1069
- s.ox = e.clientX - s.startX;
1070
- s.oy = e.clientY - s.startY;
1071
- apply();
1072
- return;
1073
- }
1074
- if (s.dragging) {
1075
- const world = screenToWorld(e.clientX, e.clientY);
1076
- const newX = Math.max(0, world.x - s.dragging.offsetX);
1077
- const newY = Math.max(0, world.y - s.dragging.offsetY);
1078
- if (!s.dragging.moved) {
1079
- const dx = Math.abs(newX - s.dragging.origX);
1080
- const dy = Math.abs(newY - s.dragging.origY);
1081
- if (dx < 2 && dy < 2) return;
1082
- }
1083
- s.dragging.moved = true;
1084
- onMoveElement(s.dragging.id, newX, newY);
1085
- return;
1086
- }
1087
- if (s.connecting) {
1088
- const inner2 = innerRef.current;
1089
- if (!inner2) return;
1090
- const world = screenToWorld(e.clientX, e.clientY);
1091
- s.connecting.x = world.x;
1092
- s.connecting.y = world.y;
1093
- // Trigger a custom event so the connecting SVG re-renders.
1094
- inner2.dispatchEvent(new CustomEvent('canvas-move', { detail: { x: world.x, y: world.y } }));
1095
- }
1096
- };
1097
- const onMouseUp = () => {
1098
- const s = stateRef.current;
1099
- if (s.panning) {
1100
- s.panning = false;
1101
- root.style.cursor = '';
1102
- }
1103
- if (s.dragging) {
1104
- if (s.dragging.moved) {
1105
- const el = elementsRef.current.find((x) => x.id === s.dragging?.id);
1106
- if (el) {
1107
- onMoveEnd([{ id: el.id, x: el.x || 0, y: el.y || 0 }]);
1108
- }
1109
- }
1110
- s.dragging = null;
1111
- }
1112
- if (s.connecting) {
1113
- s.connecting = null;
1114
- innerRef.current?.dispatchEvent(new CustomEvent('canvas-move', { detail: { x: 0, y: 0 } }));
1115
- }
1116
- };
1117
- const onWheel = (e: WheelEvent) => {
1118
- e.preventDefault();
1119
- const delta = e.deltaY > 0 ? 0.9 : 1.1;
1120
- const next = Math.min(
1121
- Math.max(stateRef.current.scale * delta, 0.25),
1122
- 2.5,
1123
- );
1124
- stateRef.current.scale = next;
1125
- apply();
1126
- };
1127
- root.addEventListener('mousedown', onMouseDown);
1128
- window.addEventListener('mousemove', onMouseMove);
1129
- window.addEventListener('mouseup', onMouseUp);
1130
- root.addEventListener('wheel', onWheel, { passive: false });
1131
- return () => {
1132
- root.removeEventListener('mousedown', onMouseDown);
1133
- window.removeEventListener('mousemove', onMouseMove);
1134
- window.removeEventListener('mouseup', onMouseUp);
1135
- root.removeEventListener('wheel', onWheel);
1136
- };
1137
- }, [onMoveElement, onMoveEnd, onSelect]);
1138
-
1139
- const fitToView = () => {
1140
- if (!rootRef.current || canvas.elements.length === 0) {
1141
- stateRef.current.ox = 0;
1142
- stateRef.current.oy = 0;
1143
- stateRef.current.scale = 1;
1144
- const inner = innerRef.current;
1145
- if (inner) {
1146
- inner.style.transform = `translate(0px, 0px) scale(1)`;
1147
- }
1148
- return;
1149
- }
1150
- const bbox = canvas.elements.reduce(
1151
- (acc, el) => {
1152
- const ex = el.x || 0;
1153
- const ey = el.y || 0;
1154
- const ex2 = ex + (el.width || 240);
1155
- const ey2 = ey + (el.height || 120);
1156
- return {
1157
- minX: Math.min(acc.minX, ex),
1158
- minY: Math.min(acc.minY, ey),
1159
- maxX: Math.max(acc.maxX, ex2),
1160
- maxY: Math.max(acc.maxY, ey2),
1161
- };
1162
- },
1163
- { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
1164
- );
1165
- const pad = 80;
1166
- const root = rootRef.current;
1167
- const w = root.clientWidth - pad * 2;
1168
- const h = root.clientHeight - pad * 2;
1169
- const bw = bbox.maxX - bbox.minX;
1170
- const bh = bbox.maxY - bbox.minY;
1171
- const scale = Math.min(w / Math.max(bw, 1), h / Math.max(bh, 1), 1.5);
1172
- const ox = -bbox.minX * scale + pad;
1173
- const oy = -bbox.minY * scale + pad;
1174
- stateRef.current.ox = ox;
1175
- stateRef.current.oy = oy;
1176
- stateRef.current.scale = scale;
1177
- const inner = innerRef.current;
1178
- if (inner) {
1179
- inner.style.transform = `translate(${ox}px, ${oy}px) scale(${scale})`;
1180
- }
1181
- };
1182
-
1183
- return (
1184
- <div className="artifacts-canvas-wrap">
1185
- <div className="canvas-toolbar">
1186
- <button type="button" className="icon-btn" title="Fit to view" aria-label="Fit canvas to view" onClick={fitToView}>
1187
- <Maximize2 size={14} />
1188
- </button>
1189
- <button
1190
- type="button"
1191
- className="icon-btn"
1192
- title="Reset zoom"
1193
- aria-label="Reset canvas zoom"
1194
- onClick={() => {
1195
- stateRef.current.ox = 0;
1196
- stateRef.current.oy = 0;
1197
- stateRef.current.scale = 1;
1198
- const inner = innerRef.current;
1199
- if (inner) inner.style.transform = 'translate(0px, 0px) scale(1)';
1200
- }}
1201
- >
1202
- <Circle size={14} />
1203
- </button>
1204
- <span className="muted text-sm" style={{ marginLeft: 'auto' }}>
1205
- Pan: drag empty · Zoom: scroll · Drag element header to move · Double-click to edit · Right-click element for actions
1206
- </span>
1207
- </div>
1208
- <div className="canvas-root" ref={rootRef} onContextMenu={(e) => {
1209
- e.preventDefault();
1210
- const rect = innerRef.current?.getBoundingClientRect();
1211
- const s = stateRef.current;
1212
- if (!rect) return;
1213
- const worldPos = { x: (e.clientX - rect.left) / s.scale, y: (e.clientY - rect.top) / s.scale };
1214
- onContextMenu?.(e, worldPos);
1215
- }}>
1216
- <div className="canvas-grid-bg" />
1217
- <div className="canvas-inner" ref={innerRef}>
1218
- {canvas.elements.length === 0 ? (
1219
- <div className="canvas-empty">
1220
- <EmptyState
1221
- icon={<MapIcon size={32} />}
1222
- title="No elements yet"
1223
- message="Add an element from the toolbar above to get started."
1224
- />
1225
- </div>
1226
- ) : (
1227
- <>
1228
- <ConnectionsLayer
1229
- canvas={canvas}
1230
- onDeleteConnection={onDeleteConnection}
1231
- />
1232
- {canvas.elements.map((el) => (
1233
- <CanvasElementView
1234
- key={el.id}
1235
- element={el}
1236
- selected={selectedElId === el.id}
1237
- commentCount={canvas.comments.filter((c) => c.elementId === el.id).length}
1238
- onSelect={() => onSelect(el.id)}
1239
- onEdit={() => onEditElement(el)}
1240
- onDelete={() => onDeleteElement(el.id)}
1241
- onContextMenu={onElementContextMenu ? (e) => {
1242
- e.preventDefault();
1243
- e.stopPropagation();
1244
- const rect = innerRef.current?.getBoundingClientRect();
1245
- if (!rect) return;
1246
- const s = stateRef.current;
1247
- const worldPos = { x: (e.clientX - rect.left) / s.scale, y: (e.clientY - rect.top) / s.scale };
1248
- onElementContextMenu(e, el.id, worldPos);
1249
- } : undefined}
1250
- />
1251
- ))}
1252
- </>
1253
- )}
1254
- </div>
1255
- </div>
1256
- </div>
1257
- );
1258
- }
1259
-
1260
- function CanvasElementView({
1261
- element,
1262
- selected,
1263
- commentCount,
1264
- onSelect,
1265
- onEdit,
1266
- onDelete,
1267
- onContextMenu,
1268
- }: {
1269
- element: CanvasElement;
1270
- selected: boolean;
1271
- commentCount: number;
1272
- onSelect: () => void;
1273
- onEdit: () => void;
1274
- onDelete: () => void;
1275
- onContextMenu?: (e: React.MouseEvent) => void;
1276
- }) {
1277
- const x = element.x || 0;
1278
- const y = element.y || 0;
1279
- const w = element.width || 240;
1280
- const h = element.height || 120;
1281
- const color = elementColor(element.type);
1282
- return (
1283
- <div
1284
- className={cn('canvas-element', selected && 'canvas-element-selected')}
1285
- data-element-id={element.id}
1286
- style={{
1287
- left: x,
1288
- top: y,
1289
- width: w,
1290
- minHeight: h,
1291
- borderColor: color,
1292
- }}
1293
- onClick={(e) => {
1294
- e.stopPropagation();
1295
- onSelect();
1296
- }}
1297
- onDoubleClick={(e) => {
1298
- e.stopPropagation();
1299
- onEdit();
1300
- }}
1301
- onContextMenu={(e) => {
1302
- if (onContextMenu) {
1303
- e.stopPropagation();
1304
- onContextMenu(e);
1305
- } else {
1306
- e.preventDefault();
1307
- if (confirm(`Delete element "${element.title || element.type}"?`)) {
1308
- onDelete();
1309
- }
1310
- }
1311
- }}
1312
- >
1313
- <div className="canvas-element-head" style={{ background: `color-mix(in srgb, ${color} 12%, transparent)` }}>
1314
- <div className="canvas-element-type">
1315
- <ElementIcon type={element.type} size={12} />
1316
- <span style={{ color }}>{element.type}</span>
1317
- </div>
1318
- <div className="canvas-element-actions">
1319
- {commentCount > 0 && (
1320
- <span className="canvas-element-badge" title={`${commentCount} comments`}>
1321
- <MessageCircle size={10} />
1322
- {commentCount}
1323
- </span>
1324
- )}
1325
- <button type="button" className="canvas-element-handle" title="Drag to move" aria-label="Drag">
1326
- ⋮⋮
1327
- </button>
1328
- </div>
1329
- </div>
1330
- <div className="canvas-element-title">{element.title || 'Untitled'}</div>
1331
- {element.content && (
1332
- <div className="canvas-element-content">{truncate(element.content, 240)}</div>
1333
- )}
1334
- {element.status && element.status !== 'open' && (
1335
- <div className="canvas-element-status">
1336
- <StatusBadge kind={element.status === 'done' ? 'success' : element.status === 'blocked' ? 'error' : 'info'}>
1337
- {element.status}
1338
- </StatusBadge>
1339
- </div>
1340
- )}
1341
- </div>
1342
- );
1343
- }
1344
-
1345
- function ConnectionsLayer({
1346
- canvas,
1347
- onDeleteConnection,
1348
- }: {
1349
- canvas: Canvas;
1350
- onDeleteConnection: (id: string) => void;
1351
- }) {
1352
- if (canvas.connections.length === 0) return null;
1353
- const els = canvas.elements;
1354
- // Compute bounding box
1355
- const bbox = els.reduce(
1356
- (acc, el) => {
1357
- const ex = el.x || 0;
1358
- const ey = el.y || 0;
1359
- const ex2 = ex + (el.width || 240);
1360
- const ey2 = ey + (el.height || 120);
1361
- return {
1362
- minX: Math.min(acc.minX, ex),
1363
- minY: Math.min(acc.minY, ey),
1364
- maxX: Math.max(acc.maxX, ex2),
1365
- maxY: Math.max(acc.maxY, ey2),
1366
- };
1367
- },
1368
- { minX: 0, minY: 0, maxX: 0, maxY: 0 },
1369
- );
1370
- const pad = 80;
1371
- const w = bbox.maxX - bbox.minX + pad * 2;
1372
- const h = bbox.maxY - bbox.minY + pad * 2;
1373
- const ox = -bbox.minX + pad;
1374
- const oy = -bbox.minY + pad;
1375
- return (
1376
- <svg
1377
- className="canvas-connections"
1378
- width={w}
1379
- height={h}
1380
- style={{ left: bbox.minX - pad, top: bbox.minY - pad }}
1381
- >
1382
- <defs>
1383
- <marker id="artifact-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
1384
- <path d="M 0 0 L 10 5 L 0 10 z" fill="var(--accent)" />
1385
- </marker>
1386
- </defs>
1387
- {canvas.connections.map((c) => {
1388
- const from = els.find((e) => e.id === c.from || e.id === c.fromElementId);
1389
- const to = els.find((e) => e.id === c.to || e.id === c.toElementId);
1390
- if (!from || !to) return null;
1391
- const x1 = ox + (from.x || 0) + (from.width || 240) / 2;
1392
- const y1 = oy + (from.y || 0) + (from.height || 120);
1393
- const x2 = ox + (to.x || 0) + (to.width || 240) / 2;
1394
- const y2 = oy + (to.y || 0);
1395
- const midX = (x1 + x2) / 2;
1396
- const midY = (y1 + y2) / 2;
1397
- return (
1398
- <g key={c.id} className="canvas-connection" onClick={() => onDeleteConnection(c.id)}>
1399
- <line x1={x1} y1={y1} x2={x2} y2={y2} stroke="var(--accent)" strokeWidth={1.5} markerEnd="url(#artifact-arrow)" />
1400
- {c.label && (
1401
- <text x={midX} y={midY} className="canvas-connection-label" textAnchor="middle">
1402
- {c.label}
1403
- </text>
1404
- )}
1405
- </g>
1406
- );
1407
- })}
1408
- </svg>
1409
- );
1410
- }
1411
-
1412
- // ── Comments panel ────────────────────────────────────────────────────
1413
-
1414
- function CommentsPanel({
1415
- slug,
1416
- comments,
1417
- selectedElId,
1418
- onAdd,
1419
- onDelete,
1420
- onClose,
1421
- }: {
1422
- slug: string;
1423
- comments: CanvasComment[];
1424
- selectedElId: string | null;
1425
- onAdd: (text: string) => void;
1426
- onDelete: (cid: string) => void;
1427
- onClose: () => void;
1428
- }) {
1429
- const [text, setText] = useState('');
1430
- return (
1431
- <aside className="artifacts-comments-panel">
1432
- <header className="artifacts-comments-panel-head">
1433
- <h3 className="card-title">
1434
- <MessageCircle size={14} />
1435
- {selectedElId ? 'Element comments' : 'Canvas comments'}
1436
- </h3>
1437
- <button type="button" className="icon-btn" onClick={onClose} title="Close panel" aria-label="Close">
1438
- <X size={14} />
1439
- </button>
1440
- </header>
1441
- <div className="artifacts-comments-panel-list">
1442
- {comments.length === 0 ? (
1443
- <p className="muted text-sm" style={{ padding: 'var(--space-4)' }}>
1444
- {selectedElId ? 'No comments on this element yet.' : 'No comments on this artifact yet.'}
1445
- </p>
1446
- ) : (
1447
- comments.map((c) => (
1448
- <div key={c.id} className="artifact-comment">
1449
- <div className="artifact-comment-head">
1450
- <span className="artifact-comment-author mono">{c.author}</span>
1451
- <span className="artifact-comment-time tabular-nums muted">{formatRelative(c.created)}</span>
1452
- <button
1453
- type="button"
1454
- className="icon-btn icon-btn-danger"
1455
- aria-label="Delete comment"
1456
- onClick={() => onDelete(c.id)}
1457
- title="Delete"
1458
- >
1459
- <Trash2 size={10} />
1460
- </button>
1461
- </div>
1462
- <div className="artifact-comment-text">{c.text}</div>
1463
- </div>
1464
- ))
1465
- )}
1466
- </div>
1467
- <form
1468
- className="artifacts-comments-panel-form"
1469
- onSubmit={(e) => {
1470
- e.preventDefault();
1471
- if (!text.trim()) return;
1472
- onAdd(text.trim());
1473
- setText('');
1474
- }}
1475
- >
1476
- <label htmlFor="artifacts-comment-input" className="sr-only">
1477
- {selectedElId ? 'Add a comment to this element' : 'Add a canvas comment'}
1478
- </label>
1479
- <textarea
1480
- id="artifacts-comment-input"
1481
- className="textarea"
1482
- rows={2}
1483
- placeholder={selectedElId ? 'Add a comment to this element…' : 'Add a canvas comment…'}
1484
- value={text}
1485
- onChange={(e) => setText(e.target.value)}
1486
- aria-label={selectedElId ? 'Add a comment to this element' : 'Add a canvas comment'}
1487
- />
1488
- <Button variant="primary" size="sm" type="submit" disabled={!text.trim()}>
1489
- <Send size={12} /> Post
1490
- </Button>
1491
- </form>
1492
- </aside>
1493
- );
1494
- }