@polderlabs/bizar 7.0.4 → 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 (489) 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 -1
  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/setup.ts +26 -0
  196. package/bizar-dash/vite.config.ts +26 -2
  197. package/bizar-dash/vitest.config.ts +5 -3
  198. package/package.json +36 -3
  199. package/bizar-dash/dist/assets/EnvVarsSection-BfhAz3ax.js +0 -114
  200. package/bizar-dash/dist/assets/EnvVarsSection-BfhAz3ax.js.map +0 -1
  201. package/bizar-dash/dist/assets/MobileChat-DLWdv6tQ.js +0 -1
  202. package/bizar-dash/dist/assets/MobileChat-DLWdv6tQ.js.map +0 -1
  203. package/bizar-dash/dist/assets/MobileSettings-Bxv62uPY.js +0 -1
  204. package/bizar-dash/dist/assets/MobileSettings-Bxv62uPY.js.map +0 -1
  205. package/bizar-dash/dist/assets/Toast-ATSAmbdi.js +0 -26
  206. package/bizar-dash/dist/assets/Toast-ATSAmbdi.js.map +0 -1
  207. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +0 -1
  208. package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +0 -1
  209. package/bizar-dash/dist/assets/main-CJAtrosO.css +0 -1
  210. package/bizar-dash/dist/assets/main-DKKj9dsq.js +0 -430
  211. package/bizar-dash/dist/assets/main-DKKj9dsq.js.map +0 -1
  212. package/bizar-dash/dist/assets/mobile-C_naUN91.js +0 -1
  213. package/bizar-dash/dist/assets/mobile-C_naUN91.js.map +0 -1
  214. package/bizar-dash/dist/assets/mobile-layout-BWLtwtfQ.css +0 -1
  215. package/bizar-dash/dist/assets/mobile-layout-GZ3bE8L6.js +0 -211
  216. package/bizar-dash/dist/assets/mobile-layout-GZ3bE8L6.js.map +0 -1
  217. package/bizar-dash/dist/assets/useSlashCommands-BOwD7lNb.js +0 -65
  218. package/bizar-dash/dist/assets/useSlashCommands-BOwD7lNb.js.map +0 -1
  219. package/bizar-dash/dist/index.html +0 -43
  220. package/bizar-dash/src/web/App.tsx +0 -915
  221. package/bizar-dash/src/web/MobileApp.tsx +0 -414
  222. package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +0 -95
  223. package/bizar-dash/src/web/components/ArtifactListDialog.tsx +0 -55
  224. package/bizar-dash/src/web/components/ArtifactViewer.tsx +0 -144
  225. package/bizar-dash/src/web/components/AuditDialog.tsx +0 -79
  226. package/bizar-dash/src/web/components/AutosaveField.tsx +0 -50
  227. package/bizar-dash/src/web/components/BackupRestore.tsx +0 -330
  228. package/bizar-dash/src/web/components/BgStatusBadge.tsx +0 -32
  229. package/bizar-dash/src/web/components/Button.tsx +0 -56
  230. package/bizar-dash/src/web/components/CanvasContextMenu.tsx +0 -68
  231. package/bizar-dash/src/web/components/Card.tsx +0 -40
  232. package/bizar-dash/src/web/components/CollapsibleSection.tsx +0 -72
  233. package/bizar-dash/src/web/components/CommandDialog.tsx +0 -66
  234. package/bizar-dash/src/web/components/DoctorPanel.tsx +0 -160
  235. package/bizar-dash/src/web/components/EmptyState.tsx +0 -30
  236. package/bizar-dash/src/web/components/EnvVarManager.tsx +0 -430
  237. package/bizar-dash/src/web/components/EvalDiff.tsx +0 -157
  238. package/bizar-dash/src/web/components/EvalExportButton.tsx +0 -27
  239. package/bizar-dash/src/web/components/EvalRunCard.tsx +0 -78
  240. package/bizar-dash/src/web/components/FileBrowser.tsx +0 -633
  241. package/bizar-dash/src/web/components/HeadroomSettings.tsx +0 -418
  242. package/bizar-dash/src/web/components/HeadroomStatus.tsx +0 -158
  243. package/bizar-dash/src/web/components/HelpDialog.tsx +0 -55
  244. package/bizar-dash/src/web/components/InviteDialog.tsx +0 -205
  245. package/bizar-dash/src/web/components/KillConfirmDialog.tsx +0 -88
  246. package/bizar-dash/src/web/components/MobileBottomNav.tsx +0 -38
  247. package/bizar-dash/src/web/components/MobileDrawer.tsx +0 -68
  248. package/bizar-dash/src/web/components/MobileHeader.tsx +0 -50
  249. package/bizar-dash/src/web/components/MobileLayout.tsx +0 -63
  250. package/bizar-dash/src/web/components/Modal.tsx +0 -215
  251. package/bizar-dash/src/web/components/Notifications.tsx +0 -242
  252. package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +0 -72
  253. package/bizar-dash/src/web/components/ScreenshotCapture.tsx +0 -138
  254. package/bizar-dash/src/web/components/ScreenshotOCR.tsx +0 -42
  255. package/bizar-dash/src/web/components/SearchModal.tsx +0 -227
  256. package/bizar-dash/src/web/components/SettingsNav.tsx +0 -117
  257. package/bizar-dash/src/web/components/SettingsSearch.tsx +0 -328
  258. package/bizar-dash/src/web/components/Sidebar.tsx +0 -89
  259. package/bizar-dash/src/web/components/Spinner.tsx +0 -19
  260. package/bizar-dash/src/web/components/StatusBadge.tsx +0 -56
  261. package/bizar-dash/src/web/components/Tag.tsx +0 -41
  262. package/bizar-dash/src/web/components/TailscaleSettings.tsx +0 -161
  263. package/bizar-dash/src/web/components/TaskCard.tsx +0 -69
  264. package/bizar-dash/src/web/components/TaskCreateSheet.tsx +0 -76
  265. package/bizar-dash/src/web/components/Toast.tsx +0 -150
  266. package/bizar-dash/src/web/components/Toggle.tsx +0 -31
  267. package/bizar-dash/src/web/components/Topbar.tsx +0 -455
  268. package/bizar-dash/src/web/components/UsageChart.tsx +0 -250
  269. package/bizar-dash/src/web/components/UsageTable.tsx +0 -90
  270. package/bizar-dash/src/web/components/VirtualList.tsx +0 -53
  271. package/bizar-dash/src/web/components/VisualPlanDialog.tsx +0 -58
  272. package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +0 -211
  273. package/bizar-dash/src/web/components/VoiceRecorder.tsx +0 -105
  274. package/bizar-dash/src/web/components/WorkspaceSelector.tsx +0 -158
  275. package/bizar-dash/src/web/components/agents/CommunicationLog.tsx +0 -126
  276. package/bizar-dash/src/web/components/agents/DependencyGraph.tsx +0 -163
  277. package/bizar-dash/src/web/components/agents/QualityGates.tsx +0 -123
  278. package/bizar-dash/src/web/components/agents/RealTimeEventLog.tsx +0 -127
  279. package/bizar-dash/src/web/components/agents/RoutingDecisions.test.tsx +0 -148
  280. package/bizar-dash/src/web/components/agents/RoutingDecisions.tsx +0 -160
  281. package/bizar-dash/src/web/components/background/AttachButton.tsx +0 -96
  282. package/bizar-dash/src/web/components/background/TmuxAttachCard.tsx +0 -122
  283. package/bizar-dash/src/web/components/chat/AgentChip.tsx +0 -68
  284. package/bizar-dash/src/web/components/chat/AgentNode.tsx +0 -127
  285. package/bizar-dash/src/web/components/chat/AgentTree.tsx +0 -80
  286. package/bizar-dash/src/web/components/chat/ChatBubble.tsx +0 -60
  287. package/bizar-dash/src/web/components/chat/ChatComposer.tsx +0 -72
  288. package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +0 -370
  289. package/bizar-dash/src/web/components/chat/ChatRail.tsx +0 -387
  290. package/bizar-dash/src/web/components/chat/ChatThread.tsx +0 -127
  291. package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +0 -50
  292. package/bizar-dash/src/web/components/chat/Composer.tsx +0 -166
  293. package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +0 -54
  294. package/bizar-dash/src/web/components/chat/EmptyState.tsx +0 -21
  295. package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +0 -18
  296. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +0 -38
  297. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +0 -184
  298. package/bizar-dash/src/web/components/chat/JumpToLatest.tsx +0 -58
  299. package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +0 -19
  300. package/bizar-dash/src/web/components/chat/MessageBlock.tsx +0 -260
  301. package/bizar-dash/src/web/components/chat/MessageBubble.tsx +0 -60
  302. package/bizar-dash/src/web/components/chat/SessionList.tsx +0 -163
  303. package/bizar-dash/src/web/components/chat/SessionRowMenu.tsx +0 -206
  304. package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +0 -37
  305. package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +0 -55
  306. package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +0 -36
  307. package/bizar-dash/src/web/components/chat/_legacy.ts +0 -30
  308. package/bizar-dash/src/web/components/chat/index.ts +0 -31
  309. package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +0 -18
  310. package/bizar-dash/src/web/components/chat/useChat.ts +0 -1190
  311. package/bizar-dash/src/web/components/chat/useSlashCommands.ts +0 -47
  312. package/bizar-dash/src/web/components/goals/GoalInput.tsx +0 -86
  313. package/bizar-dash/src/web/components/goals/PlanVisualization.tsx +0 -111
  314. package/bizar-dash/src/web/components/tasks/BacklogPanel.css +0 -109
  315. package/bizar-dash/src/web/components/tasks/BacklogPanel.tsx +0 -209
  316. package/bizar-dash/src/web/hooks/useAutosave.ts +0 -107
  317. package/bizar-dash/src/web/hooks/useI18n.ts +0 -13
  318. package/bizar-dash/src/web/index.html +0 -37
  319. package/bizar-dash/src/web/lib/api.ts +0 -237
  320. package/bizar-dash/src/web/lib/goapPlanner.ts +0 -614
  321. package/bizar-dash/src/web/lib/i18n.ts +0 -25
  322. package/bizar-dash/src/web/lib/logger.ts +0 -30
  323. package/bizar-dash/src/web/lib/markdown.tsx +0 -59
  324. package/bizar-dash/src/web/lib/search.ts +0 -115
  325. package/bizar-dash/src/web/lib/types.ts +0 -894
  326. package/bizar-dash/src/web/lib/utils.ts +0 -114
  327. package/bizar-dash/src/web/lib/ws.ts +0 -177
  328. package/bizar-dash/src/web/locales/en.json +0 -52
  329. package/bizar-dash/src/web/main.tsx +0 -43
  330. package/bizar-dash/src/web/mobile/MobileApp.tsx +0 -309
  331. package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +0 -38
  332. package/bizar-dash/src/web/mobile/MobileChat.tsx +0 -138
  333. package/bizar-dash/src/web/mobile/MobileDoctor.tsx +0 -80
  334. package/bizar-dash/src/web/mobile/MobileEval.tsx +0 -167
  335. package/bizar-dash/src/web/mobile/MobileMemory.tsx +0 -108
  336. package/bizar-dash/src/web/mobile/MobileOverview.tsx +0 -213
  337. package/bizar-dash/src/web/mobile/MobileSettings.tsx +0 -251
  338. package/bizar-dash/src/web/mobile/MobileTasks.tsx +0 -94
  339. package/bizar-dash/src/web/mobile/MobileTopbar.tsx +0 -115
  340. package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +0 -182
  341. package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +0 -64
  342. package/bizar-dash/src/web/mobile/components/MobileModal.tsx +0 -123
  343. package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +0 -273
  344. package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +0 -374
  345. package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +0 -439
  346. package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +0 -143
  347. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +0 -257
  348. package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +0 -175
  349. package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +0 -191
  350. package/bizar-dash/src/web/mobile/views/MobileMods.tsx +0 -149
  351. package/bizar-dash/src/web/mobile/views/MobileMore.tsx +0 -121
  352. package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +0 -137
  353. package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +0 -26
  354. package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +0 -685
  355. package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +0 -172
  356. package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +0 -363
  357. package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +0 -153
  358. package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +0 -564
  359. package/bizar-dash/src/web/mobile/views/QrCodePanel.tsx +0 -69
  360. package/bizar-dash/src/web/mobile.tsx +0 -14
  361. package/bizar-dash/src/web/styles/agents-redesign.css +0 -311
  362. package/bizar-dash/src/web/styles/chat.css +0 -1740
  363. package/bizar-dash/src/web/styles/glyphs.css +0 -960
  364. package/bizar-dash/src/web/styles/main.css +0 -10389
  365. package/bizar-dash/src/web/styles/memory.css +0 -1177
  366. package/bizar-dash/src/web/styles/minimax-usage.css +0 -842
  367. package/bizar-dash/src/web/styles/mobile-chat.css +0 -5
  368. package/bizar-dash/src/web/styles/mobile-layout.css +0 -233
  369. package/bizar-dash/src/web/styles/mobile.css +0 -2181
  370. package/bizar-dash/src/web/styles/settings.css +0 -775
  371. package/bizar-dash/src/web/styles/skills.css +0 -302
  372. package/bizar-dash/src/web/styles/tasks-redesign.css +0 -253
  373. package/bizar-dash/src/web/styles/tasks.css +0 -605
  374. package/bizar-dash/src/web/ui/controls/Button.tsx +0 -77
  375. package/bizar-dash/src/web/ui/controls/Checkbox.tsx +0 -95
  376. package/bizar-dash/src/web/ui/controls/IconButton.tsx +0 -66
  377. package/bizar-dash/src/web/ui/controls/Kbd.tsx +0 -24
  378. package/bizar-dash/src/web/ui/controls/NumberInput.tsx +0 -77
  379. package/bizar-dash/src/web/ui/controls/RadioGroup.tsx +0 -109
  380. package/bizar-dash/src/web/ui/controls/SearchInput.tsx +0 -91
  381. package/bizar-dash/src/web/ui/controls/Select.tsx +0 -92
  382. package/bizar-dash/src/web/ui/controls/Slider.tsx +0 -70
  383. package/bizar-dash/src/web/ui/controls/TextInput.tsx +0 -67
  384. package/bizar-dash/src/web/ui/controls/Textarea.tsx +0 -56
  385. package/bizar-dash/src/web/ui/controls/Toggle.tsx +0 -79
  386. package/bizar-dash/src/web/ui/controls/controls.css +0 -476
  387. package/bizar-dash/src/web/ui/controls/index.ts +0 -35
  388. package/bizar-dash/src/web/ui/data/BarChart.tsx +0 -60
  389. package/bizar-dash/src/web/ui/data/DataTable.tsx +0 -216
  390. package/bizar-dash/src/web/ui/data/EmptyState.tsx +0 -60
  391. package/bizar-dash/src/web/ui/data/ErrorState.tsx +0 -56
  392. package/bizar-dash/src/web/ui/data/KeyValueList.tsx +0 -106
  393. package/bizar-dash/src/web/ui/data/LoadingState.tsx +0 -65
  394. package/bizar-dash/src/web/ui/data/Sparkline.tsx +0 -87
  395. package/bizar-dash/src/web/ui/data/StatTile.tsx +0 -87
  396. package/bizar-dash/src/web/ui/data/data.css +0 -540
  397. package/bizar-dash/src/web/ui/data/index.ts +0 -31
  398. package/bizar-dash/src/web/ui/feedback/Badge.tsx +0 -49
  399. package/bizar-dash/src/web/ui/feedback/Dialog.tsx +0 -209
  400. package/bizar-dash/src/web/ui/feedback/ProgressBar.tsx +0 -69
  401. package/bizar-dash/src/web/ui/feedback/StatusDot.tsx +0 -50
  402. package/bizar-dash/src/web/ui/feedback/Toast.tsx +0 -200
  403. package/bizar-dash/src/web/ui/feedback/Tooltip.tsx +0 -126
  404. package/bizar-dash/src/web/ui/feedback/feedback.css +0 -314
  405. package/bizar-dash/src/web/ui/feedback/index.ts +0 -43
  406. package/bizar-dash/src/web/ui/index.ts +0 -57
  407. package/bizar-dash/src/web/ui/layout/AppShell.tsx +0 -51
  408. package/bizar-dash/src/web/ui/layout/Breadcrumbs.tsx +0 -97
  409. package/bizar-dash/src/web/ui/layout/Panel.tsx +0 -73
  410. package/bizar-dash/src/web/ui/layout/PanelHeader.tsx +0 -39
  411. package/bizar-dash/src/web/ui/layout/Sidebar.tsx +0 -131
  412. package/bizar-dash/src/web/ui/layout/Tabs.tsx +0 -133
  413. package/bizar-dash/src/web/ui/layout/Topbar.tsx +0 -42
  414. package/bizar-dash/src/web/ui/layout/ViewHeader.tsx +0 -48
  415. package/bizar-dash/src/web/ui/layout/index.ts +0 -34
  416. package/bizar-dash/src/web/ui/layout/layout.css +0 -464
  417. package/bizar-dash/src/web/ui/navigation/CommandPalette.tsx +0 -199
  418. package/bizar-dash/src/web/ui/navigation/NavGroup.tsx +0 -95
  419. package/bizar-dash/src/web/ui/navigation/NavLink.tsx +0 -93
  420. package/bizar-dash/src/web/ui/navigation/index.ts +0 -17
  421. package/bizar-dash/src/web/ui/navigation/navigation.css +0 -343
  422. package/bizar-dash/src/web/ui/primitives/Box.tsx +0 -91
  423. package/bizar-dash/src/web/ui/primitives/Grid.tsx +0 -49
  424. package/bizar-dash/src/web/ui/primitives/Inline.tsx +0 -55
  425. package/bizar-dash/src/web/ui/primitives/Separator.tsx +0 -46
  426. package/bizar-dash/src/web/ui/primitives/Stack.tsx +0 -54
  427. package/bizar-dash/src/web/ui/primitives/VisuallyHidden.tsx +0 -26
  428. package/bizar-dash/src/web/ui/primitives/index.ts +0 -23
  429. package/bizar-dash/src/web/ui/primitives/primitives.css +0 -183
  430. package/bizar-dash/src/web/ui/styles/globals.css +0 -86
  431. package/bizar-dash/src/web/ui/styles/reset.css +0 -125
  432. package/bizar-dash/src/web/ui/styles/tokens.css +0 -208
  433. package/bizar-dash/src/web/ui/theme/ThemeProvider.tsx +0 -142
  434. package/bizar-dash/src/web/ui/theme/tokens.ts +0 -131
  435. package/bizar-dash/src/web/ui/theme/useTheme.ts +0 -22
  436. package/bizar-dash/src/web/ui/utils/cx.ts +0 -42
  437. package/bizar-dash/src/web/views/Activity.tsx +0 -1221
  438. package/bizar-dash/src/web/views/Agents.tsx +0 -964
  439. package/bizar-dash/src/web/views/Artifacts.tsx +0 -1494
  440. package/bizar-dash/src/web/views/BackgroundAgents.tsx +0 -822
  441. package/bizar-dash/src/web/views/Chat.tsx +0 -532
  442. package/bizar-dash/src/web/views/Config.tsx +0 -3
  443. package/bizar-dash/src/web/views/Doctor.tsx +0 -317
  444. package/bizar-dash/src/web/views/Eval.tsx +0 -372
  445. package/bizar-dash/src/web/views/EvalReport.tsx +0 -353
  446. package/bizar-dash/src/web/views/GoalPlanner.tsx +0 -158
  447. package/bizar-dash/src/web/views/Harness.tsx +0 -306
  448. package/bizar-dash/src/web/views/History.tsx +0 -353
  449. package/bizar-dash/src/web/views/Memory.tsx +0 -193
  450. package/bizar-dash/src/web/views/MiniMaxUsage.tsx +0 -942
  451. package/bizar-dash/src/web/views/ModView.tsx +0 -207
  452. package/bizar-dash/src/web/views/Mods.tsx +0 -706
  453. package/bizar-dash/src/web/views/Overview.tsx +0 -1343
  454. package/bizar-dash/src/web/views/Providers.tsx +0 -302
  455. package/bizar-dash/src/web/views/Schedules.tsx +0 -902
  456. package/bizar-dash/src/web/views/Settings.tsx +0 -308
  457. package/bizar-dash/src/web/views/Skills.tsx +0 -383
  458. package/bizar-dash/src/web/views/SpawnAgentModal.tsx +0 -221
  459. package/bizar-dash/src/web/views/Tasks.tsx +0 -789
  460. package/bizar-dash/src/web/views/Workspace.tsx +0 -294
  461. package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +0 -844
  462. package/bizar-dash/src/web/views/glyphs/components.tsx +0 -1098
  463. package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +0 -426
  464. package/bizar-dash/src/web/views/memory/FromScreenshotPanel.tsx +0 -23
  465. package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +0 -220
  466. package/bizar-dash/src/web/views/memory/LightragPanel.tsx +0 -310
  467. package/bizar-dash/src/web/views/memory/MemoryGraphLegend.tsx +0 -29
  468. package/bizar-dash/src/web/views/memory/MemoryGraphPanel.tsx +0 -192
  469. package/bizar-dash/src/web/views/memory/MemoryGraphView.tsx +0 -336
  470. package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +0 -367
  471. package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +0 -160
  472. package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +0 -650
  473. package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +0 -197
  474. package/bizar-dash/src/web/views/memory/VaultFromClipboardPanel.tsx +0 -101
  475. package/bizar-dash/src/web/views/settings/ActivitySection.tsx +0 -205
  476. package/bizar-dash/src/web/views/settings/AgentSection.tsx +0 -305
  477. package/bizar-dash/src/web/views/settings/AuthSection.tsx +0 -159
  478. package/bizar-dash/src/web/views/settings/EnvVarsSection.tsx +0 -15
  479. package/bizar-dash/src/web/views/settings/GeneralSection.tsx +0 -128
  480. package/bizar-dash/src/web/views/settings/HeadroomSection.tsx +0 -39
  481. package/bizar-dash/src/web/views/settings/MemorySection.tsx +0 -154
  482. package/bizar-dash/src/web/views/settings/NetworkSection.tsx +0 -87
  483. package/bizar-dash/src/web/views/settings/NotificationsSection.tsx +0 -34
  484. package/bizar-dash/src/web/views/settings/SystemLlmSection.tsx +0 -81
  485. package/bizar-dash/src/web/views/settings/ThemeSection.tsx +0 -168
  486. package/bizar-dash/src/web/views/settings/UpdatesSection.tsx +0 -256
  487. package/bizar-dash/src/web/views/settings/WorkspacesSection.tsx +0 -189
  488. package/packages/sdk/.harness/agents.json +0 -17
  489. package/packages/sdk/.harness/topology.json +0 -4
@@ -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
- }