openclaw-node-harness 2.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 (779) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -0
  3. package/bin/discord-read.js +228 -0
  4. package/bin/fleet-deploy.js +365 -0
  5. package/bin/lane-watchdog.js +232 -0
  6. package/bin/mesh-agent.js +714 -0
  7. package/bin/mesh-bridge.js +535 -0
  8. package/bin/mesh-deploy-listener.js +322 -0
  9. package/bin/mesh-deploy.js +1048 -0
  10. package/bin/mesh-health-publisher.js +247 -0
  11. package/bin/mesh-task-daemon.js +451 -0
  12. package/bin/mesh-tool-discord.js +293 -0
  13. package/bin/mesh.js +649 -0
  14. package/boot/manifest.yaml +187 -0
  15. package/cli.js +35 -0
  16. package/config/daemon.json.template +16 -0
  17. package/config/obsidian-sync.json.template +39 -0
  18. package/config/openclaw.json.template +124 -0
  19. package/config/transcript-sources.json.template +22 -0
  20. package/identity/AGENTS.md +201 -0
  21. package/identity/CLAUDE.md +64 -0
  22. package/identity/DELEGATION.md +304 -0
  23. package/identity/HEARTBEAT.md +163 -0
  24. package/identity/MEMORY_SPEC.md +368 -0
  25. package/identity/PRINCIPLES.md +81 -0
  26. package/identity/SOUL.md +48 -0
  27. package/identity/TOOLS.md +47 -0
  28. package/install.sh +895 -0
  29. package/lib/agent-activity.js +390 -0
  30. package/lib/kanban-io.js +352 -0
  31. package/lib/mesh-registry.js +194 -0
  32. package/lib/mesh-roles.js +13 -0
  33. package/lib/mesh-tasks.js +306 -0
  34. package/lib/nats-resolve.js +108 -0
  35. package/mission-control/README.md +36 -0
  36. package/mission-control/drizzle/0000_simple_silhouette.sql +62 -0
  37. package/mission-control/drizzle/meta/0000_snapshot.json +413 -0
  38. package/mission-control/drizzle/meta/_journal.json +13 -0
  39. package/mission-control/drizzle.config.ts +13 -0
  40. package/mission-control/eslint.config.mjs +18 -0
  41. package/mission-control/next.config.ts +7 -0
  42. package/mission-control/package-lock.json +10518 -0
  43. package/mission-control/package.json +49 -0
  44. package/mission-control/postcss.config.mjs +7 -0
  45. package/mission-control/public/file.svg +1 -0
  46. package/mission-control/public/globe.svg +1 -0
  47. package/mission-control/public/next.svg +1 -0
  48. package/mission-control/public/vercel.svg +1 -0
  49. package/mission-control/public/window.svg +1 -0
  50. package/mission-control/scripts/enrich-descriptions.js +193 -0
  51. package/mission-control/scripts/gen-chronology.js +102 -0
  52. package/mission-control/scripts/import-pipeline-v2.js +523 -0
  53. package/mission-control/scripts/import-pipeline.js +295 -0
  54. package/mission-control/src/app/api/activity/live/route.ts +27 -0
  55. package/mission-control/src/app/api/activity/route.ts +47 -0
  56. package/mission-control/src/app/api/burndown/route.ts +112 -0
  57. package/mission-control/src/app/api/critical-path/route.ts +159 -0
  58. package/mission-control/src/app/api/dependencies/route.ts +176 -0
  59. package/mission-control/src/app/api/memory/categories/route.ts +93 -0
  60. package/mission-control/src/app/api/memory/consolidate/route.ts +107 -0
  61. package/mission-control/src/app/api/memory/doc/route.ts +89 -0
  62. package/mission-control/src/app/api/memory/flush/route.ts +129 -0
  63. package/mission-control/src/app/api/memory/graph/route.ts +105 -0
  64. package/mission-control/src/app/api/memory/items/route.ts +86 -0
  65. package/mission-control/src/app/api/memory/list/route.ts +48 -0
  66. package/mission-control/src/app/api/memory/retrieve/route.ts +51 -0
  67. package/mission-control/src/app/api/memory/search/route.ts +143 -0
  68. package/mission-control/src/app/api/memory/sync/route.ts +23 -0
  69. package/mission-control/src/app/api/memory/wikilinks/route.ts +75 -0
  70. package/mission-control/src/app/api/mesh/events/route.ts +67 -0
  71. package/mission-control/src/app/api/mesh/nodes/route.ts +221 -0
  72. package/mission-control/src/app/api/mesh/tokens/route.ts +133 -0
  73. package/mission-control/src/app/api/projects/route.ts +102 -0
  74. package/mission-control/src/app/api/resolve-path/route.ts +92 -0
  75. package/mission-control/src/app/api/scheduler/tick/route.ts +38 -0
  76. package/mission-control/src/app/api/scheduler/waves/route.ts +54 -0
  77. package/mission-control/src/app/api/screenshot/route.ts +127 -0
  78. package/mission-control/src/app/api/settings/gateway/route.ts +92 -0
  79. package/mission-control/src/app/api/skills/[id]/health/route.ts +57 -0
  80. package/mission-control/src/app/api/skills/list/route.ts +41 -0
  81. package/mission-control/src/app/api/souls/[id]/evolution/route.ts +253 -0
  82. package/mission-control/src/app/api/souls/[id]/prompt/route.ts +205 -0
  83. package/mission-control/src/app/api/souls/[id]/propagate/route.ts +146 -0
  84. package/mission-control/src/app/api/souls/route.ts +174 -0
  85. package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +115 -0
  86. package/mission-control/src/app/api/tasks/[id]/route.ts +266 -0
  87. package/mission-control/src/app/api/tasks/[id]/tree/route.ts +94 -0
  88. package/mission-control/src/app/api/tasks/route.ts +253 -0
  89. package/mission-control/src/app/api/tts/route.ts +47 -0
  90. package/mission-control/src/app/api/workspace/files/route.ts +88 -0
  91. package/mission-control/src/app/api/workspace/read/route.ts +73 -0
  92. package/mission-control/src/app/burndown/page.tsx +309 -0
  93. package/mission-control/src/app/calendar/page.tsx +839 -0
  94. package/mission-control/src/app/favicon.ico +0 -0
  95. package/mission-control/src/app/globals.css +67 -0
  96. package/mission-control/src/app/graph/page.tsx +352 -0
  97. package/mission-control/src/app/layout.tsx +35 -0
  98. package/mission-control/src/app/live/page.tsx +232 -0
  99. package/mission-control/src/app/memory/page.tsx +154 -0
  100. package/mission-control/src/app/mesh/page.tsx +457 -0
  101. package/mission-control/src/app/obsidian/page.tsx +252 -0
  102. package/mission-control/src/app/page.tsx +70 -0
  103. package/mission-control/src/app/roadmap/page.tsx +1757 -0
  104. package/mission-control/src/app/settings/page.tsx +260 -0
  105. package/mission-control/src/app/souls/page.tsx +573 -0
  106. package/mission-control/src/components/board/activity-timeline.tsx +96 -0
  107. package/mission-control/src/components/board/daily-board.tsx +373 -0
  108. package/mission-control/src/components/board/kanban-board.tsx +364 -0
  109. package/mission-control/src/components/board/kanban-column.tsx +105 -0
  110. package/mission-control/src/components/board/live-stream.tsx +116 -0
  111. package/mission-control/src/components/board/skill-health-card.tsx +128 -0
  112. package/mission-control/src/components/board/status-banner.tsx +124 -0
  113. package/mission-control/src/components/board/task-card.tsx +454 -0
  114. package/mission-control/src/components/board/unified-task-dialog.tsx +1043 -0
  115. package/mission-control/src/components/layout/resizable-layout.tsx +68 -0
  116. package/mission-control/src/components/layout/sidebar.tsx +90 -0
  117. package/mission-control/src/components/live/audio-spectrum.tsx +106 -0
  118. package/mission-control/src/components/live/chat-bubble.tsx +52 -0
  119. package/mission-control/src/components/live/chat-input.tsx +92 -0
  120. package/mission-control/src/components/memory/doc-reader.tsx +172 -0
  121. package/mission-control/src/components/memory/memory-list.tsx +169 -0
  122. package/mission-control/src/components/memory/search-bar.tsx +67 -0
  123. package/mission-control/src/components/memory/search-results.tsx +149 -0
  124. package/mission-control/src/components/obsidian/backlinks-panel.tsx +52 -0
  125. package/mission-control/src/components/obsidian/file-tree.tsx +186 -0
  126. package/mission-control/src/components/obsidian/local-graph.tsx +107 -0
  127. package/mission-control/src/components/obsidian/obsidian-graph.tsx +192 -0
  128. package/mission-control/src/components/obsidian/obsidian-reader.tsx +246 -0
  129. package/mission-control/src/lib/activity.ts +29 -0
  130. package/mission-control/src/lib/config.ts +21 -0
  131. package/mission-control/src/lib/db/index.ts +429 -0
  132. package/mission-control/src/lib/db/schema.ts +218 -0
  133. package/mission-control/src/lib/gateway-notify.ts +113 -0
  134. package/mission-control/src/lib/hooks.ts +536 -0
  135. package/mission-control/src/lib/memory/categories.ts +125 -0
  136. package/mission-control/src/lib/memory/entities.ts +482 -0
  137. package/mission-control/src/lib/memory/extract.ts +369 -0
  138. package/mission-control/src/lib/memory/retrieval.ts +281 -0
  139. package/mission-control/src/lib/memory/wikilinks.ts +147 -0
  140. package/mission-control/src/lib/nats.ts +126 -0
  141. package/mission-control/src/lib/parsers/clawvault-doc.ts +98 -0
  142. package/mission-control/src/lib/parsers/daily-log.ts +73 -0
  143. package/mission-control/src/lib/parsers/memory-md.ts +81 -0
  144. package/mission-control/src/lib/parsers/task-markdown.ts +459 -0
  145. package/mission-control/src/lib/parsers/transcript.ts +209 -0
  146. package/mission-control/src/lib/scheduler.ts +394 -0
  147. package/mission-control/src/lib/speech/use-speech-pipeline.ts +176 -0
  148. package/mission-control/src/lib/sync/memory.ts +224 -0
  149. package/mission-control/src/lib/sync/tasks.ts +271 -0
  150. package/mission-control/src/lib/tts/edge.ts +31 -0
  151. package/mission-control/src/lib/tts/google.ts +78 -0
  152. package/mission-control/src/lib/tts/index.ts +39 -0
  153. package/mission-control/src/lib/tts/types.ts +18 -0
  154. package/mission-control/tsconfig.json +42 -0
  155. package/obsidian-vault/.obsidian/app.json +10 -0
  156. package/obsidian-vault/.obsidian/community-plugins.json +8 -0
  157. package/obsidian-vault/.obsidian/graph.json +40 -0
  158. package/obsidian-vault/.obsidian/plugins/obsidian-local-rest-api/main.js +58769 -0
  159. package/obsidian-vault/.obsidian/plugins/obsidian-local-rest-api/manifest.json +10 -0
  160. package/obsidian-vault/.obsidian/plugins/obsidian-local-rest-api/styles.css +47 -0
  161. package/obsidian-vault/00-meta/.gitkeep +0 -0
  162. package/obsidian-vault/01-architecture/.gitkeep +0 -0
  163. package/obsidian-vault/02-smart-contracts/.gitkeep +0 -0
  164. package/obsidian-vault/03-backend/.gitkeep +0 -0
  165. package/obsidian-vault/04-mobile/.gitkeep +0 -0
  166. package/obsidian-vault/05-ar-mapping/.gitkeep +0 -0
  167. package/obsidian-vault/06-3d-assets/.gitkeep +0 -0
  168. package/obsidian-vault/07-sound-music/.gitkeep +0 -0
  169. package/obsidian-vault/08-lore/.gitkeep +0 -0
  170. package/obsidian-vault/09-quests-playthrough/.gitkeep +0 -0
  171. package/obsidian-vault/10-economy/.gitkeep +0 -0
  172. package/obsidian-vault/11-nft-assets/.gitkeep +0 -0
  173. package/obsidian-vault/12-nft-mechanics/.gitkeep +0 -0
  174. package/obsidian-vault/13-dao-guild-social/.gitkeep +0 -0
  175. package/obsidian-vault/14-game-progression/.gitkeep +0 -0
  176. package/obsidian-vault/15-analytics/.gitkeep +0 -0
  177. package/obsidian-vault/16-security/.gitkeep +0 -0
  178. package/obsidian-vault/17-devops/.gitkeep +0 -0
  179. package/obsidian-vault/18-marketplace/.gitkeep +0 -0
  180. package/obsidian-vault/19-decisions/.gitkeep +0 -0
  181. package/obsidian-vault/20-business-strategy/.gitkeep +0 -0
  182. package/obsidian-vault/21-legal-regulatory/.gitkeep +0 -0
  183. package/obsidian-vault/nodes/.gitkeep +0 -0
  184. package/openclaw.env.example +17 -0
  185. package/package.json +45 -0
  186. package/services/launchd/ai.openclaw.gateway.plist +59 -0
  187. package/services/launchd/ai.openclaw.lane-watchdog.plist +32 -0
  188. package/services/launchd/ai.openclaw.log-rotate.plist +28 -0
  189. package/services/launchd/ai.openclaw.memory-daemon.plist +36 -0
  190. package/services/launchd/ai.openclaw.mesh-agent.plist +38 -0
  191. package/services/launchd/ai.openclaw.mesh-bridge.plist +36 -0
  192. package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +33 -0
  193. package/services/launchd/ai.openclaw.mesh-health-publisher.plist +29 -0
  194. package/services/launchd/ai.openclaw.mesh-task-daemon.plist +36 -0
  195. package/services/launchd/ai.openclaw.mesh-tool-discord.plist +36 -0
  196. package/services/launchd/ai.openclaw.mission-control.plist +41 -0
  197. package/services/service-manifest.json +13 -0
  198. package/services/systemd/openclaw-gateway.service +21 -0
  199. package/services/systemd/openclaw-lane-watchdog.service +21 -0
  200. package/services/systemd/openclaw-log-rotate.service +13 -0
  201. package/services/systemd/openclaw-log-rotate.timer +9 -0
  202. package/services/systemd/openclaw-memory-daemon.service +21 -0
  203. package/services/systemd/openclaw-mesh-agent.service +19 -0
  204. package/services/systemd/openclaw-mesh-bridge.service +21 -0
  205. package/services/systemd/openclaw-mesh-deploy-listener.service +23 -0
  206. package/services/systemd/openclaw-mesh-health-publisher.service +21 -0
  207. package/services/systemd/openclaw-mesh-task-daemon.service +21 -0
  208. package/services/systemd/openclaw-mesh-tool-discord.service +21 -0
  209. package/services/systemd/openclaw-mission-control.service +22 -0
  210. package/skills/1password/.clawhub/origin.json +7 -0
  211. package/skills/1password/SKILL.md +63 -0
  212. package/skills/1password/references/cli-examples.md +29 -0
  213. package/skills/1password/references/get-started.md +17 -0
  214. package/skills/acquisition-channel-advisor/SKILL.md +643 -0
  215. package/skills/acquisition-channel-advisor/examples/conversation-flow.md +531 -0
  216. package/skills/agent-browser/.clawhub/origin.json +7 -0
  217. package/skills/agent-browser/CONTRIBUTING.md +63 -0
  218. package/skills/agent-browser/SKILL.md +338 -0
  219. package/skills/agentic-compass/.clawhub/origin.json +7 -0
  220. package/skills/agentic-compass/README.md +96 -0
  221. package/skills/agentic-compass/SKILL.md +112 -0
  222. package/skills/agentic-compass/references/README.md +5 -0
  223. package/skills/agentic-compass/scripts/agentic-compass.py +196 -0
  224. package/skills/arcane-dev-ops/SKILL.md +61 -0
  225. package/skills/arcane-dev-ops/references/checklist.md +22 -0
  226. package/skills/arcane-dev-ops/references/validation-cases.md +11 -0
  227. package/skills/arcane-dev-ops/scripts/prepush_check.sh +41 -0
  228. package/skills/auto-updater/.clawhub/origin.json +7 -0
  229. package/skills/auto-updater/SKILL.md +158 -0
  230. package/skills/auto-updater/references/agent-guide.md +152 -0
  231. package/skills/auto-updater/references/summary-examples.md +109 -0
  232. package/skills/business-health-diagnostic/SKILL.md +782 -0
  233. package/skills/byterover/.clawhub/origin.json +7 -0
  234. package/skills/byterover/SKILL.md +105 -0
  235. package/skills/byterover/TROUBLESHOOTING.md +50 -0
  236. package/skills/byterover/WORKFLOWS.md +229 -0
  237. package/skills/capability-evolver/.clawhub/origin.json +7 -0
  238. package/skills/capability-evolver/CONTRIBUTING.md +11 -0
  239. package/skills/capability-evolver/README.md +157 -0
  240. package/skills/capability-evolver/README.zh-CN.md +112 -0
  241. package/skills/capability-evolver/SKILL.md +93 -0
  242. package/skills/capability-evolver/assets/gep/capsules.json +5 -0
  243. package/skills/capability-evolver/assets/gep/genes.json +104 -0
  244. package/skills/capability-evolver/index.js +59 -0
  245. package/skills/capability-evolver/package.json +22 -0
  246. package/skills/capability-evolver/scripts/analyze_by_skill.js +121 -0
  247. package/skills/capability-evolver/scripts/build_public.js +350 -0
  248. package/skills/capability-evolver/scripts/export_history.js +98 -0
  249. package/skills/capability-evolver/scripts/extract_log.js +85 -0
  250. package/skills/capability-evolver/scripts/generate_history.js +75 -0
  251. package/skills/capability-evolver/scripts/human_report.js +147 -0
  252. package/skills/capability-evolver/scripts/publish_public.js +516 -0
  253. package/skills/capability-evolver/scripts/suggest_version.js +89 -0
  254. package/skills/capability-evolver/src/evolve.js +594 -0
  255. package/skills/capability-evolver/src/gep/assetStore.js +204 -0
  256. package/skills/capability-evolver/src/gep/candidates.js +134 -0
  257. package/skills/capability-evolver/src/gep/paths.js +23 -0
  258. package/skills/capability-evolver/src/gep/prompt.js +254 -0
  259. package/skills/capability-evolver/src/gep/selector.js +89 -0
  260. package/skills/capability-evolver/src/gep/signals.js +27 -0
  261. package/skills/cc-godmode/.clawhub/origin.json +7 -0
  262. package/skills/cc-godmode/CHANGELOG.md +66 -0
  263. package/skills/cc-godmode/README.md +293 -0
  264. package/skills/cc-godmode/SKILL.md +242 -0
  265. package/skills/cc-godmode/docs/AGENTS.md +332 -0
  266. package/skills/cc-godmode/docs/MIGRATION.md +206 -0
  267. package/skills/cc-godmode/docs/TROUBLESHOOTING.md +357 -0
  268. package/skills/cc-godmode/docs/WORKFLOWS.md +329 -0
  269. package/skills/cc-godmode/references/agents.md +433 -0
  270. package/skills/cc-godmode/scripts/build-skill.js +232 -0
  271. package/skills/clawdbot-filesystem/.clawhub/origin.json +7 -0
  272. package/skills/clawdbot-filesystem/LICENSE.md +21 -0
  273. package/skills/clawdbot-filesystem/README.md +322 -0
  274. package/skills/clawdbot-filesystem/SKILL.md +219 -0
  275. package/skills/clawdbot-filesystem/config.json +41 -0
  276. package/skills/clawdbot-filesystem/package.json +69 -0
  277. package/skills/clawdbot-security-check/.clawhub/origin.json +7 -0
  278. package/skills/clawdbot-security-check/README.md +168 -0
  279. package/skills/clawdbot-security-check/SKILL.md +145 -0
  280. package/skills/clawdbot-security-check/references/audit-checks.md +521 -0
  281. package/skills/clawdbot-security-check/skill.json +42 -0
  282. package/skills/clawddocs/.clawhub/origin.json +7 -0
  283. package/skills/clawddocs/SKILL.md +176 -0
  284. package/skills/clawddocs/package.json +9 -0
  285. package/skills/clawddocs/scripts/build-index.sh +17 -0
  286. package/skills/clawddocs/scripts/cache.sh +13 -0
  287. package/skills/clawddocs/scripts/fetch-doc.sh +7 -0
  288. package/skills/clawddocs/scripts/recent.sh +5 -0
  289. package/skills/clawddocs/scripts/search.sh +8 -0
  290. package/skills/clawddocs/scripts/sitemap.sh +23 -0
  291. package/skills/clawddocs/scripts/track-changes.sh +16 -0
  292. package/skills/clawddocs/snippets/common-configs.md +69 -0
  293. package/skills/clawguard/.clawhub/origin.json +7 -0
  294. package/skills/clawguard/SKILL.md +137 -0
  295. package/skills/company-research/SKILL.md +393 -0
  296. package/skills/company-research/examples/sample.md +164 -0
  297. package/skills/company-research/template.md +60 -0
  298. package/skills/crypto-price/.clawhub/origin.json +7 -0
  299. package/skills/crypto-price/ARCHITECTURE.md +437 -0
  300. package/skills/crypto-price/README.md +194 -0
  301. package/skills/crypto-price/SKILL.md +61 -0
  302. package/skills/crypto-price/requirements.txt +1 -0
  303. package/skills/crypto-price/scripts/get_price_chart.py +988 -0
  304. package/skills/customer-journey-map/SKILL.md +343 -0
  305. package/skills/customer-journey-map/examples/sample.md +33 -0
  306. package/skills/customer-journey-map/template.md +28 -0
  307. package/skills/customer-journey-mapping-workshop/SKILL.md +522 -0
  308. package/skills/deep-research/.clawhub/origin.json +7 -0
  309. package/skills/deep-research/SKILL.md +93 -0
  310. package/skills/deep-research/rules/logic.md +32 -0
  311. package/skills/discord-telegram-triage/SKILL.md +59 -0
  312. package/skills/discord-telegram-triage/references/discord-runbook.md +28 -0
  313. package/skills/discord-telegram-triage/references/validation-cases.md +11 -0
  314. package/skills/discord-telegram-triage/scripts/triage_snapshot.sh +23 -0
  315. package/skills/discovery-interview-prep/SKILL.md +408 -0
  316. package/skills/discovery-process/SKILL.md +503 -0
  317. package/skills/discovery-process/examples/sample.md +60 -0
  318. package/skills/discovery-process/template.md +39 -0
  319. package/skills/dist/arcane-dev-ops.skill +0 -0
  320. package/skills/dist/discord-telegram-triage.skill +0 -0
  321. package/skills/dist/founder-brief-summarizer.skill +0 -0
  322. package/skills/epic-breakdown-advisor/SKILL.md +664 -0
  323. package/skills/epic-hypothesis/SKILL.md +285 -0
  324. package/skills/epic-hypothesis/examples/sample.md +104 -0
  325. package/skills/epic-hypothesis/template.md +30 -0
  326. package/skills/excel/.clawhub/origin.json +7 -0
  327. package/skills/excel/SKILL.md +332 -0
  328. package/skills/excel/scripts/excel.py +1120 -0
  329. package/skills/fast-browser-use/.clawhub/origin.json +7 -0
  330. package/skills/fast-browser-use/CODEBUDDY.md +142 -0
  331. package/skills/fast-browser-use/Cargo.toml +77 -0
  332. package/skills/fast-browser-use/README.md +62 -0
  333. package/skills/fast-browser-use/SKILL.md +217 -0
  334. package/skills/fast-browser-use/package-lock.json +28 -0
  335. package/skills/fast-browser-use/package.json +8 -0
  336. package/skills/fast-browser-use/rustfmt.toml +10 -0
  337. package/skills/fast-browser-use/src/bin/cli.rs +373 -0
  338. package/skills/fast-browser-use/src/bin/mcp_server.rs +203 -0
  339. package/skills/fast-browser-use/src/browser/config.rs +136 -0
  340. package/skills/fast-browser-use/src/browser/debug.rs +16 -0
  341. package/skills/fast-browser-use/src/browser/mod.rs +61 -0
  342. package/skills/fast-browser-use/src/browser/session.rs +478 -0
  343. package/skills/fast-browser-use/src/dom/element.rs +442 -0
  344. package/skills/fast-browser-use/src/dom/extract_dom.js +849 -0
  345. package/skills/fast-browser-use/src/dom/mod.rs +14 -0
  346. package/skills/fast-browser-use/src/dom/tree.rs +296 -0
  347. package/skills/fast-browser-use/src/dom/yaml.rs +149 -0
  348. package/skills/fast-browser-use/src/error.rs +115 -0
  349. package/skills/fast-browser-use/src/lib.rs +17 -0
  350. package/skills/fast-browser-use/src/mcp/handler.rs +63 -0
  351. package/skills/fast-browser-use/src/mcp/mod.rs +81 -0
  352. package/skills/fast-browser-use/src/tools/Readability.min.js +1480 -0
  353. package/skills/fast-browser-use/src/tools/annotate.rs +165 -0
  354. package/skills/fast-browser-use/src/tools/click.rs +84 -0
  355. package/skills/fast-browser-use/src/tools/close.rs +35 -0
  356. package/skills/fast-browser-use/src/tools/close_tab.rs +45 -0
  357. package/skills/fast-browser-use/src/tools/convert_to_markdown.js +117 -0
  358. package/skills/fast-browser-use/src/tools/cookies.rs +58 -0
  359. package/skills/fast-browser-use/src/tools/debug.rs +44 -0
  360. package/skills/fast-browser-use/src/tools/evaluate.rs +40 -0
  361. package/skills/fast-browser-use/src/tools/extract.rs +66 -0
  362. package/skills/fast-browser-use/src/tools/go_back.rs +35 -0
  363. package/skills/fast-browser-use/src/tools/go_forward.rs +35 -0
  364. package/skills/fast-browser-use/src/tools/hover.js +33 -0
  365. package/skills/fast-browser-use/src/tools/hover.rs +97 -0
  366. package/skills/fast-browser-use/src/tools/html_to_markdown.rs +99 -0
  367. package/skills/fast-browser-use/src/tools/input.rs +93 -0
  368. package/skills/fast-browser-use/src/tools/local_storage.rs +159 -0
  369. package/skills/fast-browser-use/src/tools/markdown.rs +181 -0
  370. package/skills/fast-browser-use/src/tools/mod.rs +326 -0
  371. package/skills/fast-browser-use/src/tools/navigate.rs +55 -0
  372. package/skills/fast-browser-use/src/tools/new_tab.rs +60 -0
  373. package/skills/fast-browser-use/src/tools/press_key.rs +78 -0
  374. package/skills/fast-browser-use/src/tools/read_links.rs +59 -0
  375. package/skills/fast-browser-use/src/tools/readability_script.rs +8 -0
  376. package/skills/fast-browser-use/src/tools/screenshot.rs +47 -0
  377. package/skills/fast-browser-use/src/tools/scroll.js +22 -0
  378. package/skills/fast-browser-use/src/tools/scroll.rs +95 -0
  379. package/skills/fast-browser-use/src/tools/select.js +23 -0
  380. package/skills/fast-browser-use/src/tools/select.rs +129 -0
  381. package/skills/fast-browser-use/src/tools/sitemap.rs +426 -0
  382. package/skills/fast-browser-use/src/tools/snapshot.rs +324 -0
  383. package/skills/fast-browser-use/src/tools/switch_tab.rs +69 -0
  384. package/skills/fast-browser-use/src/tools/tab_list.rs +76 -0
  385. package/skills/fast-browser-use/src/tools/utils.rs +92 -0
  386. package/skills/fast-browser-use/src/tools/wait.rs +53 -0
  387. package/skills/fast-browser-use/test_auth.json +3 -0
  388. package/skills/fast-browser-use/test_state.json +6 -0
  389. package/skills/fast-browser-use/tests/browser_tools_integration.rs +233 -0
  390. package/skills/fast-browser-use/tests/cli_recipes_integration.rs +112 -0
  391. package/skills/fast-browser-use/tests/cookies_integration.rs +56 -0
  392. package/skills/fast-browser-use/tests/debug_integration.rs +83 -0
  393. package/skills/fast-browser-use/tests/dom_integration.rs +170 -0
  394. package/skills/fast-browser-use/tests/local_storage_integration.rs +75 -0
  395. package/skills/fast-browser-use/tests/markdown_integration.rs +448 -0
  396. package/skills/fast-browser-use/tests/navigation_integration.rs +241 -0
  397. package/skills/fast-browser-use/tests/sitemap_integration.rs +326 -0
  398. package/skills/fast-browser-use/tests/tab_management_integration.rs +300 -0
  399. package/skills/feature-investment-advisor/SKILL.md +639 -0
  400. package/skills/feature-investment-advisor/examples/conversation-flow.md +538 -0
  401. package/skills/finance-based-pricing-advisor/SKILL.md +763 -0
  402. package/skills/finance-metrics-quickref/SKILL.md +309 -0
  403. package/skills/find-skills/.clawhub/origin.json +7 -0
  404. package/skills/find-skills/SKILL.md +143 -0
  405. package/skills/flavor-text-writer/SKILL.md +27 -0
  406. package/skills/founder-brief-summarizer/SKILL.md +52 -0
  407. package/skills/founder-brief-summarizer/references/response-templates.md +15 -0
  408. package/skills/founder-brief-summarizer/references/validation-cases.md +11 -0
  409. package/skills/founder-brief-summarizer/scripts/brief_template.sh +28 -0
  410. package/skills/frontend-design/.clawhub/origin.json +7 -0
  411. package/skills/frontend-design/LICENSE.txt +190 -0
  412. package/skills/frontend-design/SKILL.md +53 -0
  413. package/skills/gemini/.clawhub/origin.json +7 -0
  414. package/skills/gemini/SKILL.md +33 -0
  415. package/skills/gemini-deep-research/.clawhub/origin.json +7 -0
  416. package/skills/gemini-deep-research/SKILL.md +78 -0
  417. package/skills/gemini-deep-research/scripts/deep_research.py +176 -0
  418. package/skills/git-essentials/.clawhub/origin.json +7 -0
  419. package/skills/git-essentials/SKILL.md +239 -0
  420. package/skills/git-essentials/references/advanced.md +211 -0
  421. package/skills/github/.clawhub/origin.json +7 -0
  422. package/skills/github/SKILL.md +57 -0
  423. package/skills/google-drive/.clawhub/origin.json +7 -0
  424. package/skills/google-drive/LICENSE.txt +21 -0
  425. package/skills/google-drive/SKILL.md +320 -0
  426. package/skills/growth-loop/SKILL.md +270 -0
  427. package/skills/growth-loop/_meta.json +9 -0
  428. package/skills/growth-loop/references/diagnosis-framework.md +84 -0
  429. package/skills/growth-loop/references/platform-benchmarks.md +79 -0
  430. package/skills/growth-loop/scripts/init-campaign.sh +274 -0
  431. package/skills/humanize-ai-text/.clawhub/origin.json +7 -0
  432. package/skills/humanize-ai-text/SKILL.md +192 -0
  433. package/skills/humanize-ai-text/scripts/compare.py +58 -0
  434. package/skills/humanize-ai-text/scripts/detect.py +160 -0
  435. package/skills/humanize-ai-text/scripts/patterns.json +191 -0
  436. package/skills/humanize-ai-text/scripts/transform.py +127 -0
  437. package/skills/humanizer/.clawhub/origin.json +7 -0
  438. package/skills/humanizer/README.md +82 -0
  439. package/skills/humanizer/SKILL.md +443 -0
  440. package/skills/jobs-to-be-done/SKILL.md +378 -0
  441. package/skills/jobs-to-be-done/examples/sample.md +80 -0
  442. package/skills/jobs-to-be-done/template.md +65 -0
  443. package/skills/lean-ux-canvas/SKILL.md +561 -0
  444. package/skills/lean-ux-canvas/examples/sample.md +88 -0
  445. package/skills/lean-ux-canvas/template.md +32 -0
  446. package/skills/markdown-formatter/.clawhub/origin.json +7 -0
  447. package/skills/markdown-formatter/README.md +137 -0
  448. package/skills/markdown-formatter/SKILL.md +369 -0
  449. package/skills/markdown-formatter/config.json +20 -0
  450. package/skills/markdown-formatter/index.js +439 -0
  451. package/skills/markdown-formatter/package.json +23 -0
  452. package/skills/markdown-formatter/test.js +23 -0
  453. package/skills/marketing-mode/.clawhub/origin.json +7 -0
  454. package/skills/marketing-mode/README.md +49 -0
  455. package/skills/marketing-mode/SKILL.md +703 -0
  456. package/skills/marketing-mode/mode-prompt.md +39 -0
  457. package/skills/marketing-mode/skill.json +51 -0
  458. package/skills/memory-hygiene/.clawhub/origin.json +7 -0
  459. package/skills/memory-hygiene/SKILL.md +91 -0
  460. package/skills/memory-setup/.clawhub/origin.json +7 -0
  461. package/skills/memory-setup/SKILL.md +180 -0
  462. package/skills/memorylayer/.clawhub/origin.json +7 -0
  463. package/skills/memorylayer/README.md +197 -0
  464. package/skills/memorylayer/SKILL.md +227 -0
  465. package/skills/memorylayer/examples/agent-integration.js +145 -0
  466. package/skills/memorylayer/examples/basic-usage.js +87 -0
  467. package/skills/memorylayer/examples/token-savings-demo.js +183 -0
  468. package/skills/memorylayer/index.js +115 -0
  469. package/skills/memorylayer/package-lock.json +295 -0
  470. package/skills/memorylayer/package.json +27 -0
  471. package/skills/memorylayer/python/memorylayer_skill.py +230 -0
  472. package/skills/memorylayer/python/requirements.txt +7 -0
  473. package/skills/mesh/SKILL.md +184 -0
  474. package/skills/model-usage/.clawhub/origin.json +7 -0
  475. package/skills/model-usage/SKILL.md +54 -0
  476. package/skills/model-usage/references/codexbar-cli.md +28 -0
  477. package/skills/model-usage/scripts/model_usage.py +310 -0
  478. package/skills/moltbook-interact/.clawhub/origin.json +7 -0
  479. package/skills/moltbook-interact/INSTALL.md +139 -0
  480. package/skills/moltbook-interact/README.md +198 -0
  481. package/skills/moltbook-interact/SKILL.md +72 -0
  482. package/skills/moltbook-interact/references/api.md +106 -0
  483. package/skills/moltbook-interact/scripts/moltbook.sh +142 -0
  484. package/skills/moltbook-registry/.clawhub/origin.json +7 -0
  485. package/skills/moltbook-registry/README.md +26 -0
  486. package/skills/moltbook-registry/SKILL.md +82 -0
  487. package/skills/moltbook-registry/index.js +180 -0
  488. package/skills/moltbook-registry/package.json +11 -0
  489. package/skills/mythril-scanner/SKILL.md +27 -0
  490. package/skills/n8n/.clawhub/origin.json +7 -0
  491. package/skills/n8n/SKILL.md +141 -0
  492. package/skills/n8n/references/api.md +156 -0
  493. package/skills/n8n/scripts/n8n_api.py +158 -0
  494. package/skills/n8n-workflow-automation/.clawhub/origin.json +7 -0
  495. package/skills/n8n-workflow-automation/SKILL.md +103 -0
  496. package/skills/n8n-workflow-automation/assets/runbook-template.md +32 -0
  497. package/skills/narrative-designer/SKILL.md +27 -0
  498. package/skills/ontology/.clawhub/origin.json +7 -0
  499. package/skills/ontology/SKILL.md +236 -0
  500. package/skills/ontology/references/queries.md +211 -0
  501. package/skills/ontology/references/schema.md +322 -0
  502. package/skills/ontology/scripts/ontology.py +374 -0
  503. package/skills/openai-image-gen/.clawhub/origin.json +7 -0
  504. package/skills/openai-image-gen/SKILL.md +45 -0
  505. package/skills/openai-image-gen/scripts/gen.py +227 -0
  506. package/skills/openclaw-agent-optimize/.clawhub/origin.json +7 -0
  507. package/skills/openclaw-agent-optimize/SKILL.md +33 -0
  508. package/skills/openclaw-agent-optimize/references/agent-orchestration.md +20 -0
  509. package/skills/openclaw-agent-optimize/references/context-management.md +15 -0
  510. package/skills/openclaw-agent-optimize/references/continuous-learning.md +14 -0
  511. package/skills/openclaw-agent-optimize/references/cron-optimization.md +16 -0
  512. package/skills/openclaw-agent-optimize/references/memory-patterns.md +14 -0
  513. package/skills/openclaw-agent-optimize/references/model-selection.md +18 -0
  514. package/skills/openclaw-skill-scanner/.clawhub/origin.json +7 -0
  515. package/skills/openclaw-skill-scanner/SKILL.md +88 -0
  516. package/skills/openclaw-skill-scanner/install-hook.sh +294 -0
  517. package/skills/openclaw-skill-scanner/report-template.md +53 -0
  518. package/skills/openclaw-skill-scanner/scanner.py +929 -0
  519. package/skills/openclaw-skill-scanner/whitelist.json +18 -0
  520. package/skills/opportunity-solution-tree/SKILL.md +428 -0
  521. package/skills/opportunity-solution-tree/examples/sample.md +104 -0
  522. package/skills/opportunity-solution-tree/template.md +33 -0
  523. package/skills/pdf/.clawhub/origin.json +7 -0
  524. package/skills/pdf/SKILL.md +304 -0
  525. package/skills/pestel-analysis/SKILL.md +384 -0
  526. package/skills/pestel-analysis/examples/sample.md +143 -0
  527. package/skills/pestel-analysis/template.md +53 -0
  528. package/skills/pol-probe/SKILL.md +217 -0
  529. package/skills/pol-probe/examples/sample.md +136 -0
  530. package/skills/pol-probe/template.md +59 -0
  531. package/skills/pol-probe-advisor/SKILL.md +492 -0
  532. package/skills/positioning-statement/SKILL.md +229 -0
  533. package/skills/positioning-statement/examples/sample.md +51 -0
  534. package/skills/positioning-statement/template.md +25 -0
  535. package/skills/positioning-workshop/SKILL.md +424 -0
  536. package/skills/prd-development/SKILL.md +654 -0
  537. package/skills/prd-development/examples/sample.md +43 -0
  538. package/skills/prd-development/template.md +55 -0
  539. package/skills/press-release/SKILL.md +277 -0
  540. package/skills/press-release/examples/sample.md +73 -0
  541. package/skills/press-release/template.md +39 -0
  542. package/skills/prioritization-advisor/SKILL.md +451 -0
  543. package/skills/proactive-messages/.clawhub/origin.json +7 -0
  544. package/skills/proactive-messages/SKILL.md +91 -0
  545. package/skills/problem-framing-canvas/SKILL.md +466 -0
  546. package/skills/problem-framing-canvas/examples/sample.md +58 -0
  547. package/skills/problem-framing-canvas/template.md +22 -0
  548. package/skills/problem-statement/SKILL.md +255 -0
  549. package/skills/problem-statement/examples/sample.md +82 -0
  550. package/skills/problem-statement/template.md +37 -0
  551. package/skills/product-strategy-session/SKILL.md +434 -0
  552. package/skills/product-strategy-session/examples/sample.md +67 -0
  553. package/skills/product-strategy-session/template.md +38 -0
  554. package/skills/prompt-guard/.clawhub/origin.json +7 -0
  555. package/skills/prompt-guard/ARCHITECTURE.md +364 -0
  556. package/skills/prompt-guard/CHANGELOG.md +200 -0
  557. package/skills/prompt-guard/README.md +215 -0
  558. package/skills/prompt-guard/SECURITY.md +66 -0
  559. package/skills/prompt-guard/SKILL.md +174 -0
  560. package/skills/prompt-guard/blog/how-i-secured-my-ai-agent.md +185 -0
  561. package/skills/prompt-guard/config.example.yaml +56 -0
  562. package/skills/prompt-guard/references/detection-patterns.md +298 -0
  563. package/skills/prompt-guard/requirements.txt +1 -0
  564. package/skills/prompt-guard/scripts/analyze_log.py +224 -0
  565. package/skills/prompt-guard/scripts/audit.py +344 -0
  566. package/skills/prompt-guard/scripts/detect.py +1587 -0
  567. package/skills/prompt-guard/scripts/hivefence.py +345 -0
  568. package/skills/proto-persona/SKILL.md +336 -0
  569. package/skills/proto-persona/examples/sample.md +97 -0
  570. package/skills/proto-persona/template.md +45 -0
  571. package/skills/recommendation-canvas/SKILL.md +382 -0
  572. package/skills/recommendation-canvas/examples/sample.md +94 -0
  573. package/skills/recommendation-canvas/template.md +86 -0
  574. package/skills/refactor-suggest/.clawhub/origin.json +7 -0
  575. package/skills/refactor-suggest/SKILL.md +94 -0
  576. package/skills/roadmap-planning/SKILL.md +506 -0
  577. package/skills/roadmap-planning/examples/sample.md +62 -0
  578. package/skills/roadmap-planning/template.md +30 -0
  579. package/skills/saas-economics-efficiency-metrics/SKILL.md +694 -0
  580. package/skills/saas-economics-efficiency-metrics/examples/cash-trap.md +365 -0
  581. package/skills/saas-economics-efficiency-metrics/examples/healthy-unit-economics.md +279 -0
  582. package/skills/saas-economics-efficiency-metrics/template.md +263 -0
  583. package/skills/saas-revenue-growth-metrics/SKILL.md +629 -0
  584. package/skills/saas-revenue-growth-metrics/examples/healthy-saas.md +131 -0
  585. package/skills/saas-revenue-growth-metrics/examples/warning-signs.md +229 -0
  586. package/skills/saas-revenue-growth-metrics/template.md +192 -0
  587. package/skills/save-money/.clawhub/origin.json +7 -0
  588. package/skills/save-money/SKILL.md +173 -0
  589. package/skills/scripts/golden_skills_v3.sh +32 -0
  590. package/skills/search/.clawhub/origin.json +7 -0
  591. package/skills/search/SKILL.md +18 -0
  592. package/skills/search/skill.json +1 -0
  593. package/skills/second-brain/.clawhub/origin.json +7 -0
  594. package/skills/second-brain/SKILL.md +278 -0
  595. package/skills/second-brain/scripts/ensue-api.sh +37 -0
  596. package/skills/self-improving-agent/.clawhub/origin.json +7 -0
  597. package/skills/self-improving-agent/.learnings/ERRORS.md +5 -0
  598. package/skills/self-improving-agent/.learnings/FEATURE_REQUESTS.md +5 -0
  599. package/skills/self-improving-agent/.learnings/LEARNINGS.md +5 -0
  600. package/skills/self-improving-agent/SKILL.md +130 -0
  601. package/skills/self-improving-agent/assets/LEARNINGS.md +45 -0
  602. package/skills/self-improving-agent/assets/SKILL-TEMPLATE.md +177 -0
  603. package/skills/self-improving-agent/hooks/openclaw/HOOK.md +23 -0
  604. package/skills/self-improving-agent/hooks/openclaw/handler.js +56 -0
  605. package/skills/self-improving-agent/hooks/openclaw/handler.ts +62 -0
  606. package/skills/self-improving-agent/references/examples.md +374 -0
  607. package/skills/self-improving-agent/references/hooks-setup.md +223 -0
  608. package/skills/self-improving-agent/references/openclaw-integration.md +248 -0
  609. package/skills/self-improving-agent/references/templates.md +480 -0
  610. package/skills/self-improving-agent/scripts/activator.sh +20 -0
  611. package/skills/self-improving-agent/scripts/error-detector.sh +55 -0
  612. package/skills/self-improving-agent/scripts/extract-skill.sh +203 -0
  613. package/skills/self-improving-agent-1-0-2/.clawhub/origin.json +7 -0
  614. package/skills/self-improving-agent-1-0-2/SKILL.md +562 -0
  615. package/skills/self-improving-agent-1-0-2/assets/LEARNINGS.md +45 -0
  616. package/skills/self-improving-agent-1-0-2/assets/SKILL-TEMPLATE.md +182 -0
  617. package/skills/self-improving-agent-1-0-2/references/clawdbot-integration.md +311 -0
  618. package/skills/self-improving-agent-1-0-2/references/examples.md +374 -0
  619. package/skills/self-improving-agent-1-0-2/references/hooks-setup.md +223 -0
  620. package/skills/self-improving-agent-1-0-2/scripts/activator.sh +20 -0
  621. package/skills/self-improving-agent-1-0-2/scripts/error-detector.sh +55 -0
  622. package/skills/self-improving-agent-1-0-2/scripts/extract-skill.sh +203 -0
  623. package/skills/self-love-confidence/.clawhub/origin.json +7 -0
  624. package/skills/self-love-confidence/SKILL.md +79 -0
  625. package/skills/self-reflection/.clawhub/origin.json +7 -0
  626. package/skills/self-reflection/README.md +292 -0
  627. package/skills/self-reflection/SKILL.md +110 -0
  628. package/skills/self-reflection/self-reflection.example.json +6 -0
  629. package/skills/slither-analyzer/SKILL.md +27 -0
  630. package/skills/solidity-audit/SKILL.md +27 -0
  631. package/skills/soulcraft/.clawhub/origin.json +7 -0
  632. package/skills/soulcraft/README.md +123 -0
  633. package/skills/soulcraft/SKILL.md +340 -0
  634. package/skills/soulcraft/references/question-bank.md +154 -0
  635. package/skills/soulcraft/references/soul-examples.md +207 -0
  636. package/skills/soulcraft/research/RESEARCH_REPORT.md +317 -0
  637. package/skills/spotify-player/.clawhub/origin.json +7 -0
  638. package/skills/spotify-player/SKILL.md +44 -0
  639. package/skills/storyboard/SKILL.md +259 -0
  640. package/skills/storyboard/examples/sample.md +71 -0
  641. package/skills/storyboard/template.md +41 -0
  642. package/skills/summarize/.clawhub/origin.json +7 -0
  643. package/skills/summarize/SKILL.md +59 -0
  644. package/skills/superdesign/.clawhub/origin.json +7 -0
  645. package/skills/superdesign/SKILL.md +224 -0
  646. package/skills/tam-sam-som-calculator/SKILL.md +399 -0
  647. package/skills/tam-sam-som-calculator/examples/sample.md +142 -0
  648. package/skills/tam-sam-som-calculator/scripts/market-sizing.py +95 -0
  649. package/skills/tam-sam-som-calculator/template.md +35 -0
  650. package/skills/tavily-search/.clawhub/origin.json +7 -0
  651. package/skills/tavily-search/SKILL.md +49 -0
  652. package/skills/tavily-search/scripts/extract.mjs +59 -0
  653. package/skills/tavily-search/scripts/search.mjs +101 -0
  654. package/skills/twitter/SKILL.md +74 -0
  655. package/skills/twitter/_meta.json +9 -0
  656. package/skills/twitter/references/validation-cases.md +53 -0
  657. package/skills/twitter/scripts/twitter.sh +421 -0
  658. package/skills/ui-ux-pro-max/.clawhub/origin.json +7 -0
  659. package/skills/ui-ux-pro-max/SKILL.md +54 -0
  660. package/skills/ui-ux-pro-max/assets/data/charts.csv +26 -0
  661. package/skills/ui-ux-pro-max/assets/data/colors.csv +97 -0
  662. package/skills/ui-ux-pro-max/assets/data/icons.csv +101 -0
  663. package/skills/ui-ux-pro-max/assets/data/landing.csv +31 -0
  664. package/skills/ui-ux-pro-max/assets/data/products.csv +97 -0
  665. package/skills/ui-ux-pro-max/assets/data/react-performance.csv +45 -0
  666. package/skills/ui-ux-pro-max/assets/data/stacks/astro.csv +54 -0
  667. package/skills/ui-ux-pro-max/assets/data/stacks/flutter.csv +53 -0
  668. package/skills/ui-ux-pro-max/assets/data/stacks/html-tailwind.csv +56 -0
  669. package/skills/ui-ux-pro-max/assets/data/stacks/jetpack-compose.csv +53 -0
  670. package/skills/ui-ux-pro-max/assets/data/stacks/nextjs.csv +53 -0
  671. package/skills/ui-ux-pro-max/assets/data/stacks/nuxt-ui.csv +51 -0
  672. package/skills/ui-ux-pro-max/assets/data/stacks/nuxtjs.csv +59 -0
  673. package/skills/ui-ux-pro-max/assets/data/stacks/react-native.csv +52 -0
  674. package/skills/ui-ux-pro-max/assets/data/stacks/react.csv +54 -0
  675. package/skills/ui-ux-pro-max/assets/data/stacks/shadcn.csv +61 -0
  676. package/skills/ui-ux-pro-max/assets/data/stacks/svelte.csv +54 -0
  677. package/skills/ui-ux-pro-max/assets/data/stacks/swiftui.csv +51 -0
  678. package/skills/ui-ux-pro-max/assets/data/stacks/vue.csv +50 -0
  679. package/skills/ui-ux-pro-max/assets/data/styles.csv +68 -0
  680. package/skills/ui-ux-pro-max/assets/data/typography.csv +58 -0
  681. package/skills/ui-ux-pro-max/assets/data/ui-reasoning.csv +101 -0
  682. package/skills/ui-ux-pro-max/assets/data/ux-guidelines.csv +100 -0
  683. package/skills/ui-ux-pro-max/assets/data/web-interface.csv +31 -0
  684. package/skills/ui-ux-pro-max/references/upstream-README.md +488 -0
  685. package/skills/ui-ux-pro-max/references/upstream-skill-content.md +288 -0
  686. package/skills/ui-ux-pro-max/scripts/__init__.py +0 -0
  687. package/skills/ui-ux-pro-max/scripts/core.py +253 -0
  688. package/skills/ui-ux-pro-max/scripts/design_system.py +1071 -0
  689. package/skills/ui-ux-pro-max/scripts/search.py +111 -0
  690. package/skills/user-story/SKILL.md +272 -0
  691. package/skills/user-story/examples/sample.md +110 -0
  692. package/skills/user-story/scripts/user-story-template.py +65 -0
  693. package/skills/user-story/template.md +32 -0
  694. package/skills/user-story-mapping/SKILL.md +296 -0
  695. package/skills/user-story-mapping/examples/sample.md +77 -0
  696. package/skills/user-story-mapping/template.md +41 -0
  697. package/skills/user-story-mapping-workshop/SKILL.md +485 -0
  698. package/skills/user-story-mapping-workshop/template.md +28 -0
  699. package/skills/user-story-splitting/SKILL.md +313 -0
  700. package/skills/user-story-splitting/examples/sample.md +147 -0
  701. package/skills/user-story-splitting/template.md +37 -0
  702. package/skills/wacli/.clawhub/origin.json +7 -0
  703. package/skills/wacli/SKILL.md +53 -0
  704. package/skills/web-search/.clawhub/origin.json +7 -0
  705. package/skills/web-search/SKILL.md +151 -0
  706. package/skills/web-search/references/api-details.md +207 -0
  707. package/skills/web-search/scripts/search.py +576 -0
  708. package/skills/workshop-facilitation/SKILL.md +88 -0
  709. package/skills/world-builder/SKILL.md +27 -0
  710. package/souls/blockchain-auditor/PRINCIPLES.md +75 -0
  711. package/souls/blockchain-auditor/SOUL.md +56 -0
  712. package/souls/blockchain-auditor/capabilities.json +33 -0
  713. package/souls/blockchain-auditor/evolution/capsules.json +4 -0
  714. package/souls/blockchain-auditor/evolution/events.jsonl +1 -0
  715. package/souls/blockchain-auditor/evolution/genes.json +62 -0
  716. package/souls/daedalus/PRINCIPLES.md +78 -0
  717. package/souls/daedalus/SOUL.md +48 -0
  718. package/souls/daedalus/capabilities.json +46 -0
  719. package/souls/identity-architect/PRINCIPLES.md +83 -0
  720. package/souls/identity-architect/SOUL.md +66 -0
  721. package/souls/identity-architect/capabilities.json +38 -0
  722. package/souls/identity-architect/evolution/capsules.json +4 -0
  723. package/souls/identity-architect/evolution/events.jsonl +0 -0
  724. package/souls/identity-architect/evolution/genes.json +4 -0
  725. package/souls/infra-ops/PRINCIPLES.md +77 -0
  726. package/souls/infra-ops/SOUL.md +56 -0
  727. package/souls/infra-ops/capabilities.json +33 -0
  728. package/souls/infra-ops/evolution/capsules.json +4 -0
  729. package/souls/infra-ops/evolution/events.jsonl +0 -0
  730. package/souls/infra-ops/evolution/genes.json +4 -0
  731. package/souls/lore-writer/PRINCIPLES.md +74 -0
  732. package/souls/lore-writer/SOUL.md +54 -0
  733. package/souls/lore-writer/capabilities.json +37 -0
  734. package/souls/lore-writer/evolution/capsules.json +4 -0
  735. package/souls/lore-writer/evolution/events.jsonl +0 -0
  736. package/souls/lore-writer/evolution/genes.json +4 -0
  737. package/souls/qa-evidence/PRINCIPLES.md +97 -0
  738. package/souls/qa-evidence/SOUL.md +66 -0
  739. package/souls/qa-evidence/capabilities.json +32 -0
  740. package/souls/qa-evidence/evolution/capsules.json +4 -0
  741. package/souls/qa-evidence/evolution/events.jsonl +0 -0
  742. package/souls/qa-evidence/evolution/genes.json +4 -0
  743. package/souls/registry.json +211 -0
  744. package/souls/sync-registry.js +65 -0
  745. package/uninstall.sh +102 -0
  746. package/workspace-bin/auto-checkpoint +60 -0
  747. package/workspace-bin/clawvault-access-control +65 -0
  748. package/workspace-bin/clawvault-local +28 -0
  749. package/workspace-bin/compile-boot +494 -0
  750. package/workspace-bin/daily-log-writer.mjs +251 -0
  751. package/workspace-bin/evolve +540 -0
  752. package/workspace-bin/fitness_score.py +395 -0
  753. package/workspace-bin/hooks/pre-commit +80 -0
  754. package/workspace-bin/install-daemon +299 -0
  755. package/workspace-bin/lane-watchdog.js +232 -0
  756. package/workspace-bin/lib/__init__.py +0 -0
  757. package/workspace-bin/lib/frontmatter.py +114 -0
  758. package/workspace-bin/memory-daemon.mjs +879 -0
  759. package/workspace-bin/memory-maintenance.mjs +531 -0
  760. package/workspace-bin/mesh-bridge.mjs +154 -0
  761. package/workspace-bin/multi-review +130 -0
  762. package/workspace-bin/obsidian +125 -0
  763. package/workspace-bin/obsidian-sync.mjs +888 -0
  764. package/workspace-bin/openclaw-register-source +102 -0
  765. package/workspace-bin/proactive-scan +147 -0
  766. package/workspace-bin/quality-gate +175 -0
  767. package/workspace-bin/screenshot +96 -0
  768. package/workspace-bin/session-recap +453 -0
  769. package/workspace-bin/skill-audit +494 -0
  770. package/workspace-bin/skill-quality-check +134 -0
  771. package/workspace-bin/skill-routing-eval +599 -0
  772. package/workspace-bin/soul-prompt +251 -0
  773. package/workspace-bin/subagent-audit.mjs +267 -0
  774. package/workspace-bin/test-multi-soul-workflow +130 -0
  775. package/workspace-bin/trust-registry +465 -0
  776. package/workspace-docs/AGENTS.md +201 -0
  777. package/workspace-docs/CLAUDE.md +64 -0
  778. package/workspace-docs/PRINCIPLES.md +81 -0
  779. package/workspace-docs/SOUL.md +48 -0
@@ -0,0 +1,1757 @@
1
+ "use client";
2
+
3
+ import { useState, useMemo, useRef, useCallback, useEffect, Fragment } from "react";
4
+ import {
5
+ format,
6
+ addMonths,
7
+ startOfMonth,
8
+ endOfMonth,
9
+ eachDayOfInterval,
10
+ eachMonthOfInterval,
11
+ differenceInDays,
12
+ isToday,
13
+ isWeekend,
14
+ addDays,
15
+ parseISO,
16
+ startOfWeek,
17
+ getQuarter,
18
+ startOfQuarter,
19
+ isSameMonth,
20
+ isBefore,
21
+ startOfDay,
22
+ } from "date-fns";
23
+ import {
24
+ Plus,
25
+ ChevronRight,
26
+ ChevronDown,
27
+ FolderKanban,
28
+ GitBranch,
29
+ Layers,
30
+ CheckSquare,
31
+ ZoomIn,
32
+ ZoomOut,
33
+ Filter,
34
+ X,
35
+ Diamond,
36
+ AlertTriangle,
37
+ Search,
38
+ ArrowLeftFromLine,
39
+ ArrowRightFromLine,
40
+ Clock,
41
+ } from "lucide-react";
42
+ import {
43
+ useProjects,
44
+ useProjectTree,
45
+ useProjectDependencies,
46
+ useCriticalPath,
47
+ updateTask,
48
+ createDependency,
49
+ type Task,
50
+ type Dependency,
51
+ } from "@/lib/hooks";
52
+ import { UnifiedTaskDialog } from "@/components/board/unified-task-dialog";
53
+
54
+ // --- Constants ---
55
+
56
+ const ROW_HEIGHT = 36;
57
+ const DEFAULT_LABEL_WIDTH = 300;
58
+ const MIN_LABEL_WIDTH = 160;
59
+ const MAX_LABEL_WIDTH = 600;
60
+ const TYPE_ICONS: Record<string, typeof FolderKanban> = {
61
+ project: FolderKanban,
62
+ pipeline: GitBranch,
63
+ phase: Layers,
64
+ task: CheckSquare,
65
+ };
66
+
67
+ const STATUS_COLORS: Record<string, string> = {
68
+ running: "#22c55e",
69
+ blocked: "#ef4444",
70
+ "waiting-user": "#eab308",
71
+ queued: "#3b82f6",
72
+ done: "#71717a",
73
+ cancelled: "#52525b",
74
+ };
75
+
76
+ type ZoomLevel = "day" | "week" | "month" | "quarter";
77
+
78
+ // Department color map (matches import-pipeline-v2.js)
79
+ const DEPT_COLORS: Record<string, string> = {
80
+ DEV: "#3B82F6",
81
+ CHAIN: "#8B5CF6",
82
+ ART: "#EC4899",
83
+ DESIGN: "#F59E0B",
84
+ NARR: "#14B8A6",
85
+ QA: "#10B981",
86
+ INFRA: "#6B7280",
87
+ MKT: "#EF4444",
88
+ COMM: "#06B6D4",
89
+ BIZ: "#22C55E",
90
+ LEGAL: "#F97316",
91
+ HIRE: "#A855F7",
92
+ };
93
+
94
+ /** Extract estimated hours from description (e.g. "DEV · 5.0h · M1/D3" → "5.0h") */
95
+ function extractEstimate(description: string | null): string | null {
96
+ if (!description) return null;
97
+ const parts = description.split(" · ");
98
+ for (const p of parts) {
99
+ if (/^\d+(\.\d+)?h$/.test(p.trim())) return p.trim();
100
+ }
101
+ return null;
102
+ }
103
+
104
+ /** Extract department from a task's description field (e.g. "DEV" or "⚠️ CRITICAL PATH — DEV") */
105
+ function extractDept(description: string | null): string | null {
106
+ if (!description) return null;
107
+ // Check for "CRITICAL PATH — DEPT" or "IMPORTANT — DEPT" pattern
108
+ const tagMatch = description.match(/(?:CRITICAL PATH|IMPORTANT)\s*[—–-]\s*(\w+)/);
109
+ if (tagMatch) return tagMatch[1];
110
+ // Otherwise treat the whole description as dept if it matches a known dept
111
+ const trimmed = description.trim();
112
+ if (DEPT_COLORS[trimmed]) return trimmed;
113
+ return null;
114
+ }
115
+
116
+ // --- Helper: build tree structure from flat list ---
117
+
118
+ interface TreeNode extends Task {
119
+ children: TreeNode[];
120
+ depth: number;
121
+ }
122
+
123
+ function buildTree(items: Task[]): TreeNode[] {
124
+ const map = new Map<string, TreeNode>();
125
+ const roots: TreeNode[] = [];
126
+
127
+ for (const item of items) {
128
+ map.set(item.id, { ...item, children: [], depth: 0 });
129
+ }
130
+
131
+ for (const node of map.values()) {
132
+ if (node.parentId && map.has(node.parentId)) {
133
+ const parent = map.get(node.parentId)!;
134
+ node.depth = parent.depth + 1;
135
+ parent.children.push(node);
136
+ } else {
137
+ roots.push(node);
138
+ }
139
+ }
140
+
141
+ // Sort children chronologically by date (not alphabetically by ID/dept)
142
+ const dateSortChildren = (nodes: TreeNode[]) => {
143
+ nodes.sort((a, b) => {
144
+ const aDate = a.scheduledDate || a.startDate || "";
145
+ const bDate = b.scheduledDate || b.startDate || "";
146
+ if (aDate !== bDate) return aDate.localeCompare(bDate);
147
+ return a.id.localeCompare(b.id);
148
+ });
149
+ for (const n of nodes) {
150
+ if (n.children.length > 0) dateSortChildren(n.children);
151
+ }
152
+ };
153
+ dateSortChildren(roots);
154
+
155
+ return roots;
156
+ }
157
+
158
+ function flattenTree(
159
+ nodes: TreeNode[],
160
+ collapsed: Set<string>
161
+ ): TreeNode[] {
162
+ const result: TreeNode[] = [];
163
+ function walk(items: TreeNode[]) {
164
+ for (const node of items) {
165
+ result.push(node);
166
+ if (!collapsed.has(node.id) && node.children.length > 0) {
167
+ walk(node.children);
168
+ }
169
+ }
170
+ }
171
+ walk(nodes);
172
+ return result;
173
+ }
174
+
175
+ // --- Component ---
176
+
177
+ export default function RoadmapPage() {
178
+ const { projects, isLoading: projectsLoading } = useProjects();
179
+ const [selectedProject, setSelectedProject] = useState<string | null>(null);
180
+ const { tree: treeData } = useProjectTree(selectedProject);
181
+ const { dependencies } = useProjectDependencies(selectedProject);
182
+ const { criticalPath } = useCriticalPath(selectedProject);
183
+ const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
184
+ const [zoom, setZoom] = useState<ZoomLevel>("month");
185
+ const [viewStart, setViewStart] = useState(startOfMonth(new Date()));
186
+ const [dialogOpen, setDialogOpen] = useState(false);
187
+ const [editItem, setEditItem] = useState<Task | null>(null);
188
+ const [dialogType, setDialogType] = useState<"project" | "pipeline" | "phase">("project");
189
+ const [dialogParentId, setDialogParentId] = useState<string | null>(null);
190
+ const [depMode, setDepMode] = useState<string | null>(null); // source id for dependency creation
191
+ const [labelWidth, setLabelWidth] = useState(DEFAULT_LABEL_WIDTH);
192
+ const [selectedDepts, setSelectedDepts] = useState<Set<string>>(new Set()); // empty = show all
193
+ const [filterOpen, setFilterOpen] = useState(false);
194
+ const [ganttSearch, setGanttSearch] = useState("");
195
+ const [ganttSearchResults, setGanttSearchResults] = useState<TreeNode[]>([]);
196
+ const [ganttSearchOpen, setGanttSearchOpen] = useState(false);
197
+ const [ganttSearchIdx, setGanttSearchIdx] = useState(0);
198
+ const [showCriticalOnly, setShowCriticalOnly] = useState(false);
199
+ const ganttSearchRef = useRef<HTMLInputElement>(null);
200
+ const labelPanelRef = useRef<HTMLDivElement>(null);
201
+ const ganttRef = useRef<HTMLDivElement>(null);
202
+ const stripRef = useRef<HTMLDivElement>(null);
203
+ const stripDragRef = useRef<{ startX: number; startScrollLeft: number } | null>(null);
204
+ const stripWasDragged = useRef(false);
205
+ const resizingRef = useRef<{ startX: number; startWidth: number } | null>(null);
206
+
207
+ // Auto-select first project
208
+ useEffect(() => {
209
+ if (!selectedProject && projects.length > 0) {
210
+ setSelectedProject(projects[0].id);
211
+ }
212
+ }, [projects, selectedProject]);
213
+
214
+ // Build tree from flat data
215
+ const treeRoots = useMemo(() => buildTree(treeData), [treeData]);
216
+ const flatRows = useMemo(
217
+ () => flattenTree(treeRoots, collapsed),
218
+ [treeRoots, collapsed]
219
+ );
220
+
221
+ // Extract all unique departments from tree data
222
+ const allDepts = useMemo(() => {
223
+ const depts = new Set<string>();
224
+ for (const t of treeData) {
225
+ const dept = extractDept(t.description);
226
+ if (dept) depts.add(dept);
227
+ }
228
+ return Array.from(depts).sort();
229
+ }, [treeData]);
230
+
231
+ // Critical path set for O(1) lookups
232
+ const criticalPathSet = useMemo(() => new Set(criticalPath), [criticalPath]);
233
+
234
+ // Progress rollup for parent nodes
235
+ const progressMap = useMemo(() => {
236
+ const map = new Map<string, { done: number; total: number }>();
237
+ function compute(node: TreeNode): { done: number; total: number } {
238
+ if (node.children.length === 0) {
239
+ const isDone = node.status === "done" || node.status === "cancelled";
240
+ return { done: isDone ? 1 : 0, total: 1 };
241
+ }
242
+ let done = 0, total = 0;
243
+ for (const child of node.children) {
244
+ const cp = compute(child);
245
+ done += cp.done;
246
+ total += cp.total;
247
+ }
248
+ map.set(node.id, { done, total });
249
+ return { done, total };
250
+ }
251
+ for (const root of treeRoots) compute(root);
252
+ return map;
253
+ }, [treeRoots]);
254
+
255
+ // Blocked-by map: targetId → list of unfinished predecessors
256
+ const blockedByMap = useMemo(() => {
257
+ const map = new Map<string, Array<{ id: string; title: string }>>();
258
+ const taskMap = new Map(treeData.map((t) => [t.id, t]));
259
+ for (const dep of dependencies) {
260
+ const source = taskMap.get(dep.sourceId);
261
+ if (source && source.status !== "done" && source.status !== "cancelled") {
262
+ const existing = map.get(dep.targetId) || [];
263
+ existing.push({ id: source.id, title: source.title });
264
+ map.set(dep.targetId, existing);
265
+ }
266
+ }
267
+ return map;
268
+ }, [dependencies, treeData]);
269
+
270
+ // Predecessors map: taskId → list of source tasks (what must finish before this)
271
+ const predecessorMap = useMemo(() => {
272
+ const map = new Map<string, Array<{ id: string; title: string; status: string }>>();
273
+ const taskMap = new Map(treeData.map((t) => [t.id, t]));
274
+ for (const dep of dependencies) {
275
+ const source = taskMap.get(dep.sourceId);
276
+ if (source) {
277
+ const existing = map.get(dep.targetId) || [];
278
+ existing.push({ id: source.id, title: source.title, status: source.status });
279
+ map.set(dep.targetId, existing);
280
+ }
281
+ }
282
+ return map;
283
+ }, [dependencies, treeData]);
284
+
285
+ // Successors map: taskId → list of target tasks (what depends on this)
286
+ const successorMap = useMemo(() => {
287
+ const map = new Map<string, Array<{ id: string; title: string; status: string }>>();
288
+ const taskMap = new Map(treeData.map((t) => [t.id, t]));
289
+ for (const dep of dependencies) {
290
+ const target = taskMap.get(dep.targetId);
291
+ if (target) {
292
+ const existing = map.get(dep.sourceId) || [];
293
+ existing.push({ id: target.id, title: target.title, status: target.status });
294
+ map.set(dep.sourceId, existing);
295
+ }
296
+ }
297
+ return map;
298
+ }, [dependencies, treeData]);
299
+
300
+ // Today reference for overdue detection
301
+ const todayDate = useMemo(() => startOfDay(new Date()), []);
302
+
303
+ // All nodes for search (including collapsed ones)
304
+ const allNodes = useMemo(() => flattenTree(treeRoots, new Set()), [treeRoots]);
305
+
306
+ // Gantt search: find matches across ALL nodes (not just visible)
307
+ useEffect(() => {
308
+ if (!ganttSearch.trim()) {
309
+ // Use functional update to avoid new [] reference on every render
310
+ // (prevents infinite loop when allNodes changes due to SWR revalidation)
311
+ setGanttSearchResults((prev) => (prev.length === 0 ? prev : []));
312
+ setGanttSearchOpen(false);
313
+ return;
314
+ }
315
+ const q = ganttSearch.toLowerCase();
316
+ const matches = allNodes.filter(
317
+ (n) =>
318
+ n.title.toLowerCase().includes(q) ||
319
+ n.id.toLowerCase().includes(q) ||
320
+ (n.description || "").toLowerCase().includes(q) ||
321
+ (n.owner || "").toLowerCase().includes(q)
322
+ );
323
+ setGanttSearchResults(matches);
324
+ setGanttSearchOpen(matches.length > 0);
325
+ setGanttSearchIdx(0);
326
+ }, [ganttSearch, allNodes]);
327
+
328
+ // Navigate to a specific task: uncollapse ancestors, scroll both panels
329
+ const navigateToTask = useCallback(
330
+ (targetId: string) => {
331
+ // Find the node in treeData to get its parent chain
332
+ const nodeMap = new Map(treeData.map((t) => [t.id, t]));
333
+ // Uncollapse all ancestors
334
+ const newCollapsed = new Set(collapsed);
335
+ let current = nodeMap.get(targetId);
336
+ while (current?.parentId) {
337
+ newCollapsed.delete(current.parentId);
338
+ current = nodeMap.get(current.parentId);
339
+ }
340
+ setCollapsed(newCollapsed);
341
+
342
+ // Move timeline to task's start date FIRST so the bar renders in view
343
+ const task = nodeMap.get(targetId);
344
+ if (task?.startDate) {
345
+ const taskStart = parseISO(task.startDate);
346
+ // Center the view on the task start (back up ~2 weeks so bar is visible)
347
+ setViewStart(addDays(startOfMonth(taskStart), -7));
348
+ }
349
+
350
+ // Scroll after state update (need a tick for re-render + timeline shift)
351
+ setTimeout(() => {
352
+ const rowEl = document.querySelector(`[data-task-id="${targetId}"]`);
353
+ const ganttRowEl = document.querySelector(`[data-gantt-id="${targetId}"]`);
354
+
355
+ if (rowEl && labelPanelRef.current) {
356
+ // Scroll label panel to the row
357
+ const panelRect = labelPanelRef.current.getBoundingClientRect();
358
+ const rowRect = rowEl.getBoundingClientRect();
359
+ const scrollTarget = labelPanelRef.current.scrollTop + (rowRect.top - panelRect.top) - panelRect.height / 2;
360
+ labelPanelRef.current.scrollTo({ top: scrollTarget, behavior: "smooth" });
361
+
362
+ // Sync gantt vertical scroll
363
+ if (ganttRef.current) {
364
+ ganttRef.current.scrollTo({ top: scrollTarget, behavior: "smooth" });
365
+ }
366
+
367
+ // Flash highlight on label row
368
+ const highlightClasses = ["ring-2", "ring-primary", "bg-primary/20", "z-10"];
369
+ rowEl.classList.add(...highlightClasses);
370
+ setTimeout(() => rowEl.classList.remove(...highlightClasses), 20000);
371
+ }
372
+
373
+ // Flash highlight on gantt bar row
374
+ if (ganttRowEl) {
375
+ const barEl = ganttRowEl.querySelector("div[class*='group']") as HTMLElement;
376
+ if (barEl) {
377
+ barEl.classList.add("ring-2", "ring-primary", "shadow-[0_0_12px_rgba(var(--primary),0.5)]");
378
+ setTimeout(() => {
379
+ barEl.classList.remove("ring-2", "ring-primary", "shadow-[0_0_12px_rgba(var(--primary),0.5)]");
380
+ }, 20000);
381
+ }
382
+ // Also highlight the row background
383
+ ganttRowEl.classList.add("bg-primary/10");
384
+ setTimeout(() => ganttRowEl.classList.remove("bg-primary/10"), 20000);
385
+ }
386
+
387
+ // Scroll gantt horizontally to the bar
388
+ if (ganttRef.current && ganttRowEl) {
389
+ const barInner = ganttRowEl.querySelector("[class*='cursor-grab'], [class*='rotate-45']") as HTMLElement;
390
+ if (barInner) {
391
+ const ganttRect = ganttRef.current.getBoundingClientRect();
392
+ const barRect = barInner.getBoundingClientRect();
393
+ const scrollLeft = ganttRef.current.scrollLeft + (barRect.left - ganttRect.left) - ganttRect.width / 3;
394
+ ganttRef.current.scrollTo({ left: Math.max(0, scrollLeft), behavior: "smooth" });
395
+ }
396
+ }
397
+ }, 150);
398
+
399
+ setGanttSearch("");
400
+ setGanttSearchOpen(false);
401
+ },
402
+ [collapsed, treeData]
403
+ );
404
+
405
+ // Keyboard shortcut: / to focus search
406
+ useEffect(() => {
407
+ const handler = (e: KeyboardEvent) => {
408
+ if (
409
+ e.target instanceof HTMLInputElement ||
410
+ e.target instanceof HTMLTextAreaElement ||
411
+ e.target instanceof HTMLSelectElement
412
+ ) return;
413
+ if (e.key === "/" && !dialogOpen) {
414
+ e.preventDefault();
415
+ ganttSearchRef.current?.focus();
416
+ }
417
+ if (e.key === "Escape" && ganttSearch) {
418
+ setGanttSearch("");
419
+ setGanttSearchOpen(false);
420
+ }
421
+ };
422
+ window.addEventListener("keydown", handler);
423
+ return () => window.removeEventListener("keydown", handler);
424
+ }, [dialogOpen, ganttSearch]);
425
+
426
+ // Filter rows by selected departments and/or critical path
427
+ const filteredRows = useMemo(() => {
428
+ const hasDeptFilter = selectedDepts.size > 0;
429
+ if (!hasDeptFilter && !showCriticalOnly) return flatRows;
430
+
431
+ // Collect IDs of tasks matching the filters
432
+ const matchingIds = new Set<string>();
433
+ for (const node of flatRows) {
434
+ const deptMatch = !hasDeptFilter || (extractDept(node.description) && selectedDepts.has(extractDept(node.description)!));
435
+ const critMatch = !showCriticalOnly || criticalPathSet.has(node.id);
436
+ if (deptMatch && critMatch) {
437
+ matchingIds.add(node.id);
438
+ }
439
+ }
440
+
441
+ // Walk up parentId chain to keep ancestor nodes visible
442
+ const visibleIds = new Set(matchingIds);
443
+ const idToNode = new Map(flatRows.map((n) => [n.id, n]));
444
+ for (const id of matchingIds) {
445
+ let current = idToNode.get(id);
446
+ while (current?.parentId) {
447
+ visibleIds.add(current.parentId);
448
+ current = idToNode.get(current.parentId);
449
+ }
450
+ }
451
+
452
+ return flatRows.filter((n) => visibleIds.has(n.id));
453
+ }, [flatRows, selectedDepts, showCriticalOnly, criticalPathSet]);
454
+
455
+ // Timeline range
456
+ const viewEnd = useMemo(() => {
457
+ if (zoom === "day") return addDays(viewStart, 14);
458
+ if (zoom === "week") return addDays(viewStart, 28);
459
+ if (zoom === "quarter") return addMonths(viewStart, 6);
460
+ return addMonths(viewStart, 3); // month view = 3 months
461
+ }, [viewStart, zoom]);
462
+
463
+ const totalDays = useMemo(
464
+ () => differenceInDays(viewEnd, viewStart),
465
+ [viewStart, viewEnd]
466
+ );
467
+
468
+ // Day width based on zoom
469
+ const dayWidth = useMemo(() => {
470
+ if (zoom === "day") return 90;
471
+ if (zoom === "week") return 40;
472
+ if (zoom === "quarter") return 6;
473
+ return 14; // month
474
+ }, [zoom]);
475
+
476
+ const baseTimelineWidth = totalDays * dayWidth;
477
+
478
+ // Extend container to fit bars that extend beyond the view window
479
+ const timelineWidth = useMemo(() => {
480
+ let maxRight = baseTimelineWidth;
481
+ for (const node of filteredRows) {
482
+ if (!node.startDate && !(node.type === "task" && node.scheduledDate)) continue;
483
+ let barRight = 0;
484
+ if (node.type === "task" && node.scheduledDate) {
485
+ const date = parseISO(node.scheduledDate);
486
+ barRight = differenceInDays(date, viewStart) * dayWidth + Math.max(dayWidth, 60);
487
+ } else if (node.startDate) {
488
+ const start = parseISO(node.startDate);
489
+ const end = node.endDate ? parseISO(node.endDate) : addDays(start, 7);
490
+ barRight = differenceInDays(start, viewStart) * dayWidth + Math.max(differenceInDays(end, start) * dayWidth, dayWidth * 2);
491
+ }
492
+ maxRight = Math.max(maxRight, barRight + 220);
493
+ }
494
+ return maxRight;
495
+ }, [filteredRows, viewStart, dayWidth, baseTimelineWidth]);
496
+
497
+ // Month markers for the header
498
+ const months = useMemo(
499
+ () => eachMonthOfInterval({ start: viewStart, end: viewEnd }),
500
+ [viewStart, viewEnd]
501
+ );
502
+
503
+ // Day markers for day/week zoom
504
+ const days = useMemo(() => {
505
+ if (zoom !== "day" && zoom !== "week") return [];
506
+ return eachDayOfInterval({ start: viewStart, end: addDays(viewEnd, -1) });
507
+ }, [viewStart, viewEnd, zoom]);
508
+
509
+ // Today line position
510
+ const todayOffset = useMemo(() => {
511
+ const days = differenceInDays(new Date(), viewStart);
512
+ if (days < 0 || days > totalDays) return null;
513
+ return days * dayWidth;
514
+ }, [viewStart, totalDays, dayWidth]);
515
+
516
+ // Project date range for the date strip
517
+ const projectRange = useMemo(() => {
518
+ let earliest = new Date();
519
+ let latest = addMonths(new Date(), 3);
520
+ for (const task of treeData) {
521
+ for (const dateStr of [task.startDate, task.scheduledDate, task.endDate]) {
522
+ if (dateStr) {
523
+ const d = parseISO(dateStr);
524
+ if (d < earliest) earliest = d;
525
+ if (d > latest) latest = d;
526
+ }
527
+ }
528
+ }
529
+ return {
530
+ start: startOfMonth(addMonths(earliest, -1)),
531
+ end: endOfMonth(addMonths(latest, 1)),
532
+ };
533
+ }, [treeData]);
534
+
535
+ // Selection range end (the highlighted unit in the strip)
536
+ const selectionEnd = useMemo(() => {
537
+ switch (zoom) {
538
+ case "day": return addDays(viewStart, 1);
539
+ case "week": return addDays(viewStart, 7);
540
+ case "month": return addMonths(viewStart, 1);
541
+ case "quarter": return addMonths(viewStart, 3);
542
+ }
543
+ }, [viewStart, zoom]);
544
+
545
+ // Strip always shows individual days across the full project range
546
+ const stripDays = useMemo(() => {
547
+ return eachDayOfInterval({ start: projectRange.start, end: projectRange.end });
548
+ }, [projectRange]);
549
+
550
+ // Strip → Gantt coordinate alignment: pixel offset from strip origin to viewStart
551
+ const stripOriginOffset = useMemo(
552
+ () => differenceInDays(viewStart, projectRange.start) * dayWidth,
553
+ [viewStart, projectRange.start, dayWidth]
554
+ );
555
+
556
+ const stripTotalWidth = useMemo(() => {
557
+ const projectWidth = differenceInDays(projectRange.end, projectRange.start) * dayWidth;
558
+ return Math.max(projectWidth, timelineWidth + stripOriginOffset) + 200;
559
+ }, [projectRange, dayWidth, timelineWidth, stripOriginOffset]);
560
+
561
+ const stripMonths = useMemo(
562
+ () => eachMonthOfInterval({ start: projectRange.start, end: projectRange.end }),
563
+ [projectRange]
564
+ );
565
+
566
+ // Navigation label shows the selected unit (not the full visible range)
567
+ const viewRangeLabel = useMemo(() => {
568
+ switch (zoom) {
569
+ case "day":
570
+ return format(viewStart, "EEE, MMM d yyyy");
571
+ case "week": {
572
+ const weekEnd = addDays(viewStart, 6);
573
+ if (isSameMonth(viewStart, weekEnd)) {
574
+ return `${format(viewStart, "MMM d")} — ${format(weekEnd, "d, yyyy")}`;
575
+ }
576
+ return `${format(viewStart, "MMM d")} — ${format(weekEnd, "MMM d, yyyy")}`;
577
+ }
578
+ case "month":
579
+ return format(viewStart, "MMMM yyyy");
580
+ case "quarter":
581
+ return `Q${getQuarter(viewStart)} ${format(viewStart, "yyyy")}`;
582
+ }
583
+ }, [viewStart, zoom]);
584
+
585
+ // Sync strip scroll position to match Gantt view origin
586
+ useEffect(() => {
587
+ if (stripRef.current) {
588
+ stripRef.current.scrollLeft = stripOriginOffset;
589
+ }
590
+ }, [stripOriginOffset]);
591
+
592
+ // Initialize --gantt-scroll-left CSS variable for sticky bar labels
593
+ useEffect(() => {
594
+ if (ganttRef.current) {
595
+ ganttRef.current.style.setProperty(
596
+ "--gantt-scroll-left",
597
+ ganttRef.current.scrollLeft + "px"
598
+ );
599
+ }
600
+ }, []);
601
+
602
+ // --- Helpers for bar positioning ---
603
+
604
+ const getBarPosition = useCallback(
605
+ (item: Task) => {
606
+ // Leaf tasks: use scheduledDate for single-day positioning
607
+ if (item.type === "task" && item.scheduledDate) {
608
+ const date = parseISO(item.scheduledDate);
609
+ const startOffset = differenceInDays(date, viewStart) * dayWidth;
610
+ // Single-day bar with minimum width for readability
611
+ const width = Math.max(dayWidth, 60);
612
+ return { left: startOffset, width };
613
+ }
614
+ if (!item.startDate) return null;
615
+ const start = parseISO(item.startDate);
616
+ const end = item.endDate ? parseISO(item.endDate) : addDays(start, 7);
617
+ const startOffset = differenceInDays(start, viewStart) * dayWidth;
618
+ const width = Math.max(differenceInDays(end, start) * dayWidth, dayWidth * 2);
619
+ return { left: startOffset, width };
620
+ },
621
+ [viewStart, dayWidth]
622
+ );
623
+
624
+ // Parent options for dialog
625
+ const parentOptions = useMemo(() => {
626
+ return treeData
627
+ .filter((t) => t.type === "project" || t.type === "pipeline")
628
+ .map((t) => ({ id: t.id, title: t.title, type: t.type || "task" }));
629
+ }, [treeData]);
630
+
631
+ // --- Handlers ---
632
+
633
+ const toggleCollapse = (id: string) => {
634
+ setCollapsed((prev) => {
635
+ const next = new Set(prev);
636
+ if (next.has(id)) next.delete(id);
637
+ else next.add(id);
638
+ return next;
639
+ });
640
+ };
641
+
642
+ const handleBarDrag = useCallback(
643
+ async (taskId: string, newLeft: number, newWidth: number) => {
644
+ const startDays = Math.round(newLeft / dayWidth);
645
+ const durationDays = Math.max(Math.round(newWidth / dayWidth), 1);
646
+ const newStart = addDays(viewStart, startDays);
647
+ const newEnd = addDays(newStart, durationDays);
648
+ await updateTask(taskId, {
649
+ start_date: format(newStart, "yyyy-MM-dd"),
650
+ end_date: format(newEnd, "yyyy-MM-dd"),
651
+ } as Record<string, unknown>);
652
+ },
653
+ [viewStart, dayWidth]
654
+ );
655
+
656
+ const handleAddDep = async (targetId: string) => {
657
+ if (!depMode || depMode === targetId) {
658
+ setDepMode(null);
659
+ return;
660
+ }
661
+ try {
662
+ await createDependency({ sourceId: depMode, targetId });
663
+ } catch {
664
+ // cycle or other error — silently ignore
665
+ }
666
+ setDepMode(null);
667
+ };
668
+
669
+ const toggleDept = (dept: string) => {
670
+ setSelectedDepts((prev) => {
671
+ const next = new Set(prev);
672
+ if (next.has(dept)) next.delete(dept);
673
+ else next.add(dept);
674
+ return next;
675
+ });
676
+ };
677
+
678
+ const navigateTimeline = (direction: number) => {
679
+ if (zoom === "day") setViewStart(addDays(viewStart, direction));
680
+ else if (zoom === "week") setViewStart(addDays(viewStart, 7 * direction));
681
+ else if (zoom === "quarter") setViewStart(addMonths(viewStart, 3 * direction));
682
+ else setViewStart(addMonths(viewStart, direction));
683
+ };
684
+
685
+ // Strip: grab-to-pan drag handler
686
+ const handleStripMouseDown = useCallback((e: React.MouseEvent) => {
687
+ const container = stripRef.current;
688
+ if (!container) return;
689
+ stripDragRef.current = { startX: e.clientX, startScrollLeft: container.scrollLeft };
690
+ stripWasDragged.current = false;
691
+
692
+ const handleMove = (ev: MouseEvent) => {
693
+ if (!stripDragRef.current) return;
694
+ const dx = ev.clientX - stripDragRef.current.startX;
695
+ if (Math.abs(dx) > 3) stripWasDragged.current = true;
696
+ container.scrollLeft = stripDragRef.current.startScrollLeft - dx;
697
+ };
698
+
699
+ const handleUp = () => {
700
+ stripDragRef.current = null;
701
+ document.removeEventListener("mousemove", handleMove);
702
+ document.removeEventListener("mouseup", handleUp);
703
+ document.body.style.cursor = "";
704
+ };
705
+
706
+ document.addEventListener("mousemove", handleMove);
707
+ document.addEventListener("mouseup", handleUp);
708
+ document.body.style.cursor = "grabbing";
709
+ }, []);
710
+
711
+ // Strip: click a day to jump to it (snaps to containing unit)
712
+ const handleStripDayClick = useCallback((date: Date) => {
713
+ if (stripWasDragged.current) return;
714
+ switch (zoom) {
715
+ case "day": setViewStart(date); break;
716
+ case "week": setViewStart(startOfWeek(date, { weekStartsOn: 1 })); break;
717
+ case "month": setViewStart(startOfMonth(date)); break;
718
+ case "quarter": setViewStart(startOfQuarter(date)); break;
719
+ }
720
+ }, [zoom]);
721
+
722
+ // Timeline header height depends on zoom level — match left panel header to it
723
+ const headerHeight = (zoom === "day" || zoom === "week") ? 44 : 24;
724
+
725
+ // --- Vertical scroll sync between label panel and gantt ---
726
+ const syncingScroll = useRef(false);
727
+ const handleLabelScroll = useCallback(() => {
728
+ if (syncingScroll.current) return;
729
+ syncingScroll.current = true;
730
+ if (labelPanelRef.current && ganttRef.current) {
731
+ ganttRef.current.scrollTop = labelPanelRef.current.scrollTop;
732
+ }
733
+ syncingScroll.current = false;
734
+ }, []);
735
+ const handleGanttScroll = useCallback(() => {
736
+ if (syncingScroll.current) return;
737
+ syncingScroll.current = true;
738
+ if (ganttRef.current) {
739
+ if (labelPanelRef.current) {
740
+ labelPanelRef.current.scrollTop = ganttRef.current.scrollTop;
741
+ }
742
+ if (stripRef.current) {
743
+ stripRef.current.scrollLeft = ganttRef.current.scrollLeft + stripOriginOffset;
744
+ }
745
+ // Update CSS variable for sticky bar labels
746
+ ganttRef.current.style.setProperty(
747
+ "--gantt-scroll-left",
748
+ ganttRef.current.scrollLeft + "px"
749
+ );
750
+ }
751
+ syncingScroll.current = false;
752
+ }, [stripOriginOffset]);
753
+
754
+ const handleStripScroll = useCallback(() => {
755
+ if (syncingScroll.current) return;
756
+ syncingScroll.current = true;
757
+ if (stripRef.current && ganttRef.current) {
758
+ ganttRef.current.scrollLeft = stripRef.current.scrollLeft - stripOriginOffset;
759
+ }
760
+ syncingScroll.current = false;
761
+ }, [stripOriginOffset]);
762
+
763
+ // --- Resize handle for left panel ---
764
+ const handleResizeStart = useCallback((e: React.MouseEvent) => {
765
+ e.preventDefault();
766
+ resizingRef.current = { startX: e.clientX, startWidth: labelWidth };
767
+
768
+ const handleMouseMove = (ev: MouseEvent) => {
769
+ if (!resizingRef.current) return;
770
+ const dx = ev.clientX - resizingRef.current.startX;
771
+ const newWidth = Math.min(
772
+ MAX_LABEL_WIDTH,
773
+ Math.max(MIN_LABEL_WIDTH, resizingRef.current.startWidth + dx)
774
+ );
775
+ setLabelWidth(newWidth);
776
+ };
777
+
778
+ const handleMouseUp = () => {
779
+ resizingRef.current = null;
780
+ document.removeEventListener("mousemove", handleMouseMove);
781
+ document.removeEventListener("mouseup", handleMouseUp);
782
+ document.body.style.cursor = "";
783
+ document.body.style.userSelect = "";
784
+ };
785
+
786
+ document.addEventListener("mousemove", handleMouseMove);
787
+ document.addEventListener("mouseup", handleMouseUp);
788
+ document.body.style.cursor = "col-resize";
789
+ document.body.style.userSelect = "none";
790
+ }, [labelWidth]);
791
+
792
+ // --- Render ---
793
+
794
+ if (projectsLoading) {
795
+ return (
796
+ <div className="h-full flex items-center justify-center text-muted-foreground text-sm">
797
+ Loading...
798
+ </div>
799
+ );
800
+ }
801
+
802
+ if (projects.length === 0) {
803
+ return (
804
+ <div className="h-full flex flex-col items-center justify-center gap-4">
805
+ <FolderKanban className="h-12 w-12 text-muted-foreground/30" />
806
+ <p className="text-muted-foreground">No projects yet</p>
807
+ <button
808
+ onClick={() => {
809
+ setDialogType("project");
810
+ setEditItem(null);
811
+ setDialogParentId(null);
812
+ setDialogOpen(true);
813
+ }}
814
+ className="flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
815
+ >
816
+ <Plus className="h-4 w-4" />
817
+ Create First Project
818
+ </button>
819
+ <UnifiedTaskDialog
820
+ item={null}
821
+ open={dialogOpen}
822
+ onClose={() => setDialogOpen(false)}
823
+ defaultType="project"
824
+ parentOptions={[]}
825
+ />
826
+ </div>
827
+ );
828
+ }
829
+
830
+ return (
831
+ <div className="h-full flex flex-col">
832
+ {/* Header */}
833
+ <header className="border-b border-border px-6 py-3 flex items-center justify-between shrink-0">
834
+ <div className="flex items-center gap-4">
835
+ <h1 className="text-xl font-semibold text-foreground">Roadmap</h1>
836
+
837
+ {/* Project selector */}
838
+ <select
839
+ value={selectedProject ?? ""}
840
+ onChange={(e) => setSelectedProject(e.target.value || null)}
841
+ className="rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
842
+ >
843
+ {projects.map((p) => (
844
+ <option key={p.id} value={p.id}>
845
+ {p.title}
846
+ </option>
847
+ ))}
848
+ </select>
849
+
850
+ {/* Timeline navigation */}
851
+ <div className="flex items-center gap-1 ml-2">
852
+ <button
853
+ onClick={() => navigateTimeline(-1)}
854
+ className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
855
+ >
856
+ <ChevronRight className="h-4 w-4 rotate-180" />
857
+ </button>
858
+ <span className="text-xs text-muted-foreground min-w-[140px] text-center">
859
+ {viewRangeLabel}
860
+ </span>
861
+ <button
862
+ onClick={() => navigateTimeline(1)}
863
+ className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
864
+ >
865
+ <ChevronRight className="h-4 w-4" />
866
+ </button>
867
+ <button
868
+ onClick={() => setViewStart(startOfMonth(new Date()))}
869
+ className="text-xs text-muted-foreground hover:text-foreground px-2 py-1 rounded hover:bg-accent transition-colors ml-1"
870
+ >
871
+ Today
872
+ </button>
873
+ </div>
874
+ </div>
875
+
876
+ <div className="flex items-center gap-2">
877
+ {/* Zoom controls */}
878
+ <div className="flex items-center gap-0.5 border border-border rounded-md overflow-hidden">
879
+ {(["day", "week", "month", "quarter"] as ZoomLevel[]).map((z) => (
880
+ <button
881
+ key={z}
882
+ onClick={() => setZoom(z)}
883
+ className={`px-2.5 py-1 text-xs transition-colors ${
884
+ zoom === z
885
+ ? "bg-primary text-primary-foreground"
886
+ : "text-muted-foreground hover:text-foreground hover:bg-accent"
887
+ }`}
888
+ >
889
+ {z.charAt(0).toUpperCase() + z.slice(1)}
890
+ </button>
891
+ ))}
892
+ </div>
893
+
894
+ {/* Critical path toggle */}
895
+ {criticalPath.length > 0 && (
896
+ <button
897
+ onClick={() => setShowCriticalOnly((p) => !p)}
898
+ className={`flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md border transition-colors ${
899
+ showCriticalOnly
900
+ ? "border-red-500 bg-red-500/15 text-red-400"
901
+ : "border-border text-muted-foreground hover:text-foreground hover:bg-accent"
902
+ }`}
903
+ >
904
+ <AlertTriangle className="h-3 w-3" />
905
+ {showCriticalOnly ? `Critical (${criticalPath.length})` : "Critical Path"}
906
+ </button>
907
+ )}
908
+
909
+ {/* Department filter */}
910
+ {allDepts.length > 0 && (
911
+ <div className="relative">
912
+ <button
913
+ onClick={() => setFilterOpen(!filterOpen)}
914
+ className={`flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md border transition-colors ${
915
+ selectedDepts.size > 0
916
+ ? "border-primary bg-primary/10 text-primary"
917
+ : "border-border text-muted-foreground hover:text-foreground hover:bg-accent"
918
+ }`}
919
+ >
920
+ <Filter className="h-3 w-3" />
921
+ {selectedDepts.size > 0 ? `${selectedDepts.size} dept${selectedDepts.size > 1 ? "s" : ""}` : "Filter"}
922
+ </button>
923
+
924
+ {filterOpen && (
925
+ <>
926
+ <div className="fixed inset-0 z-40" onClick={() => setFilterOpen(false)} />
927
+ <div className="absolute right-0 top-full mt-1 z-50 border border-border rounded-lg shadow-lg p-2 min-w-[180px]" style={{ backgroundColor: "var(--card)" }}>
928
+ <div className="flex items-center justify-between px-2 pb-1.5 mb-1 border-b border-border">
929
+ <span className="text-xs font-medium text-foreground">Departments</span>
930
+ {selectedDepts.size > 0 && (
931
+ <button
932
+ onClick={() => setSelectedDepts(new Set())}
933
+ className="text-[10px] text-muted-foreground hover:text-foreground"
934
+ >
935
+ Clear all
936
+ </button>
937
+ )}
938
+ </div>
939
+ {allDepts.map((dept) => (
940
+ <button
941
+ key={dept}
942
+ onClick={() => toggleDept(dept)}
943
+ className={`flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded-md transition-colors ${
944
+ selectedDepts.has(dept)
945
+ ? "bg-accent text-foreground"
946
+ : "text-muted-foreground hover:text-foreground hover:bg-accent/50"
947
+ }`}
948
+ >
949
+ <div
950
+ className="w-2.5 h-2.5 rounded-sm shrink-0"
951
+ style={{ backgroundColor: DEPT_COLORS[dept] || "#71717a" }}
952
+ />
953
+ <span className="flex-1 text-left">{dept}</span>
954
+ {selectedDepts.has(dept) && <X className="h-3 w-3" />}
955
+ </button>
956
+ ))}
957
+ </div>
958
+ </>
959
+ )}
960
+ </div>
961
+ )}
962
+
963
+ {/* Add buttons */}
964
+ <button
965
+ onClick={() => {
966
+ setDialogType("project");
967
+ setEditItem(null);
968
+ setDialogParentId(null);
969
+ setDialogOpen(true);
970
+ }}
971
+ className="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
972
+ >
973
+ <Plus className="h-3 w-3" />
974
+ New
975
+ </button>
976
+ </div>
977
+ </header>
978
+
979
+ {/* Date strip — aligned with Gantt timeline, synced scroll */}
980
+ <div className="border-b border-border shrink-0 flex">
981
+ {/* Left spacer matching label panel width */}
982
+ <div className="shrink-0 border-r border-border" style={{ width: labelWidth }} />
983
+ {/* Scrollable strip synced with Gantt */}
984
+ <div
985
+ ref={stripRef}
986
+ className="flex-1 overflow-x-auto py-0.5 cursor-grab active:cursor-grabbing select-none"
987
+ onMouseDown={handleStripMouseDown}
988
+ onScroll={handleStripScroll}
989
+ >
990
+ <div className="relative" style={{ width: stripTotalWidth, height: 22 }}>
991
+ {/* Month boundary labels */}
992
+ {stripMonths.map((month) => {
993
+ const offset = differenceInDays(month, projectRange.start) * dayWidth;
994
+ return (
995
+ <span
996
+ key={`sm-${month.toISOString()}`}
997
+ className="absolute top-0 text-[9px] text-muted-foreground/50 font-medium whitespace-nowrap border-l border-border/40 pl-1"
998
+ style={{ left: offset }}
999
+ >
1000
+ {format(month, "MMM")}
1001
+ </span>
1002
+ );
1003
+ })}
1004
+ {/* Day buttons — positioned at same scale as Gantt */}
1005
+ {stripDays.map((date) => {
1006
+ const offset = differenceInDays(date, projectRange.start) * dayWidth;
1007
+ const inSelection = date >= viewStart && date < selectionEnd;
1008
+ const today = isToday(date);
1009
+ const weekend = isWeekend(date);
1010
+ const isFirst = date.getDate() === 1;
1011
+ // At small dayWidth, only show some labels to avoid overlap
1012
+ const showLabel = dayWidth >= 16 || isFirst || (dayWidth >= 8 && date.getDate() % 5 === 0);
1013
+
1014
+ return (
1015
+ <button
1016
+ key={date.toISOString()}
1017
+ data-active={inSelection || undefined}
1018
+ onClick={() => handleStripDayClick(date)}
1019
+ className={`absolute text-[10px] text-center transition-colors ${
1020
+ inSelection
1021
+ ? "text-primary font-semibold border-b-2 border-primary"
1022
+ : today
1023
+ ? "text-primary"
1024
+ : weekend
1025
+ ? "text-muted-foreground/40"
1026
+ : "text-muted-foreground/70 hover:text-foreground"
1027
+ }${today ? " bg-primary/10 rounded-t-sm" : ""}`}
1028
+ style={{ left: offset, width: dayWidth, top: 8, height: 14 }}
1029
+ >
1030
+ {showLabel ? format(date, "d") : ""}
1031
+ </button>
1032
+ );
1033
+ })}
1034
+ </div>
1035
+ </div>
1036
+ </div>
1037
+
1038
+ {/* Main content: tree labels + gantt chart */}
1039
+ <div className="flex-1 flex overflow-hidden">
1040
+ {/* Left panel: tree labels */}
1041
+ <div
1042
+ ref={labelPanelRef}
1043
+ className="shrink-0 overflow-y-auto bg-card"
1044
+ style={{ width: labelWidth }}
1045
+ onScroll={handleLabelScroll}
1046
+ >
1047
+ {/* Column header with search — height matches timeline header */}
1048
+ <div className="border-b border-border flex items-center px-2 gap-1.5 relative sticky top-0 z-10 bg-card" style={{ height: headerHeight }}>
1049
+ <Search className="h-3 w-3 text-muted-foreground shrink-0" />
1050
+ <input
1051
+ ref={ganttSearchRef}
1052
+ type="text"
1053
+ value={ganttSearch}
1054
+ onChange={(e) => setGanttSearch(e.target.value)}
1055
+ onKeyDown={(e) => {
1056
+ if (e.key === "Escape") {
1057
+ setGanttSearch("");
1058
+ setGanttSearchOpen(false);
1059
+ (e.target as HTMLInputElement).blur();
1060
+ } else if (e.key === "ArrowDown" && ganttSearchOpen) {
1061
+ e.preventDefault();
1062
+ setGanttSearchIdx((i) => Math.min(i + 1, ganttSearchResults.length - 1));
1063
+ } else if (e.key === "ArrowUp" && ganttSearchOpen) {
1064
+ e.preventDefault();
1065
+ setGanttSearchIdx((i) => Math.max(i - 1, 0));
1066
+ } else if (e.key === "Enter" && ganttSearchOpen && ganttSearchResults.length > 0) {
1067
+ e.preventDefault();
1068
+ navigateToTask(ganttSearchResults[ganttSearchIdx].id);
1069
+ }
1070
+ }}
1071
+ placeholder="Search tasks... (/)"
1072
+ className="flex-1 min-w-0 bg-transparent text-xs text-foreground placeholder:text-muted-foreground/50 focus:outline-none"
1073
+ />
1074
+ {ganttSearch && (
1075
+ <button
1076
+ onClick={() => { setGanttSearch(""); setGanttSearchOpen(false); }}
1077
+ className="text-muted-foreground hover:text-foreground shrink-0"
1078
+ >
1079
+ <X className="h-3 w-3" />
1080
+ </button>
1081
+ )}
1082
+
1083
+ {/* Search results dropdown */}
1084
+ {ganttSearchOpen && ganttSearchResults.length > 0 && (
1085
+ <div className="absolute left-0 top-full z-50 w-full max-h-[280px] overflow-y-auto border border-border rounded-b-lg shadow-xl" style={{ backgroundColor: "var(--card)" }}>
1086
+ <div className="px-2 py-1 text-[10px] text-muted-foreground border-b border-border">
1087
+ {ganttSearchResults.length} result{ganttSearchResults.length !== 1 ? "s" : ""}
1088
+ </div>
1089
+ {ganttSearchResults.slice(0, 50).map((node, i) => {
1090
+ const dept = extractDept(node.description);
1091
+ return (
1092
+ <button
1093
+ key={node.id}
1094
+ onClick={() => navigateToTask(node.id)}
1095
+ className={`w-full text-left px-2 py-1.5 flex items-center gap-2 text-xs transition-colors ${
1096
+ i === ganttSearchIdx
1097
+ ? "bg-primary/15 text-foreground"
1098
+ : "text-muted-foreground hover:bg-accent hover:text-foreground"
1099
+ }`}
1100
+ >
1101
+ {dept && (
1102
+ <div
1103
+ className="w-2 h-2 rounded-sm shrink-0"
1104
+ style={{ backgroundColor: DEPT_COLORS[dept] || "#71717a" }}
1105
+ />
1106
+ )}
1107
+ <span className="truncate flex-1">{node.title}</span>
1108
+ <span className="text-[9px] text-muted-foreground/50 shrink-0">{node.id}</span>
1109
+ </button>
1110
+ );
1111
+ })}
1112
+ </div>
1113
+ )}
1114
+ </div>
1115
+
1116
+ {filteredRows.map((node) => {
1117
+ const Icon = TYPE_ICONS[node.type || "task"] || CheckSquare;
1118
+ const hasChildren = node.children.length > 0;
1119
+ const isCollapsed = collapsed.has(node.id);
1120
+ const progress = progressMap.get(node.id);
1121
+ const blockers = blockedByMap.get(node.id);
1122
+ const preds = predecessorMap.get(node.id);
1123
+ const succs = successorMap.get(node.id);
1124
+ const isCritical = criticalPathSet.has(node.id);
1125
+ const isOverdue = !!(
1126
+ node.endDate &&
1127
+ isBefore(parseISO(node.endDate), todayDate) &&
1128
+ node.status !== "done" &&
1129
+ node.status !== "cancelled"
1130
+ );
1131
+ const estimate = extractEstimate(node.description);
1132
+
1133
+ return (
1134
+ <div
1135
+ key={node.id}
1136
+ data-task-id={node.id}
1137
+ className={`flex items-center gap-1.5 border-b border-border/50 hover:bg-accent/30 transition-colors cursor-pointer group ${
1138
+ depMode ? "cursor-crosshair" : ""
1139
+ }${isCritical ? " bg-red-500/15 border-l-2 border-l-red-500" : ""}${isOverdue ? " bg-orange-500/10" : ""}`}
1140
+ style={{
1141
+ height: ROW_HEIGHT,
1142
+ paddingLeft: 8 + node.depth * 20,
1143
+ }}
1144
+ onClick={() => {
1145
+ if (depMode) {
1146
+ handleAddDep(node.id);
1147
+ } else {
1148
+ setEditItem(node);
1149
+ setDialogType(
1150
+ (node.type as "project" | "pipeline" | "phase") || "project"
1151
+ );
1152
+ setDialogParentId(node.parentId);
1153
+ setDialogOpen(true);
1154
+ }
1155
+ }}
1156
+ >
1157
+ {/* Collapse toggle */}
1158
+ {hasChildren ? (
1159
+ <button
1160
+ onClick={(e) => {
1161
+ e.stopPropagation();
1162
+ toggleCollapse(node.id);
1163
+ }}
1164
+ className="text-muted-foreground hover:text-foreground p-0.5"
1165
+ >
1166
+ {isCollapsed ? (
1167
+ <ChevronRight className="h-3 w-3" />
1168
+ ) : (
1169
+ <ChevronDown className="h-3 w-3" />
1170
+ )}
1171
+ </button>
1172
+ ) : (
1173
+ <span className="w-4" />
1174
+ )}
1175
+
1176
+ {node.type === "milestone" ? (
1177
+ <Diamond className="h-3.5 w-3.5 shrink-0 text-amber-400" />
1178
+ ) : (
1179
+ <Icon
1180
+ className="h-3.5 w-3.5 shrink-0"
1181
+ style={{ color: node.color || "#71717a" }}
1182
+ />
1183
+ )}
1184
+ <span className={`text-xs truncate flex-1 ${
1185
+ isCritical ? "text-red-400 font-semibold" : "text-foreground"
1186
+ }`}>
1187
+ {node.title}
1188
+ </span>
1189
+
1190
+ {/* Dependency counts */}
1191
+ {preds && preds.length > 0 && (
1192
+ <span className="relative group/pred shrink-0">
1193
+ <span className="inline-flex items-center gap-0.5 text-[9px] text-blue-400 tabular-nums">
1194
+ <ArrowLeftFromLine className="h-2.5 w-2.5" />
1195
+ {preds.length}
1196
+ </span>
1197
+ <span className="absolute hidden group-hover/pred:block right-0 top-full z-50 bg-popover border border-border rounded-md p-2 shadow-lg text-[10px] text-foreground whitespace-nowrap min-w-[180px]">
1198
+ <span className="font-semibold block mb-1 text-blue-400">Depends on:</span>
1199
+ {preds.map((p) => (
1200
+ <span key={p.id} className="flex items-center gap-1.5 py-0.5">
1201
+ <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${p.status === "done" ? "bg-green-400" : "bg-orange-400"}`} />
1202
+ <span className="text-muted-foreground truncate">{p.title}</span>
1203
+ </span>
1204
+ ))}
1205
+ </span>
1206
+ </span>
1207
+ )}
1208
+ {succs && succs.length > 0 && (
1209
+ <span className="relative group/succ shrink-0">
1210
+ <span className="inline-flex items-center gap-0.5 text-[9px] text-purple-400 tabular-nums">
1211
+ <ArrowRightFromLine className="h-2.5 w-2.5" />
1212
+ {succs.length}
1213
+ </span>
1214
+ <span className="absolute hidden group-hover/succ:block right-0 top-full z-50 bg-popover border border-border rounded-md p-2 shadow-lg text-[10px] text-foreground whitespace-nowrap min-w-[180px]">
1215
+ <span className="font-semibold block mb-1 text-purple-400">Blocks:</span>
1216
+ {succs.map((s) => (
1217
+ <span key={s.id} className="flex items-center gap-1.5 py-0.5">
1218
+ <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${s.status === "done" ? "bg-green-400" : "bg-zinc-400"}`} />
1219
+ <span className="text-muted-foreground truncate">{s.title}</span>
1220
+ </span>
1221
+ ))}
1222
+ </span>
1223
+ </span>
1224
+ )}
1225
+
1226
+ {/* Critical path badge */}
1227
+ {isCritical && (
1228
+ <span className="text-[9px] text-red-400 font-bold shrink-0 px-1 py-0.5 rounded bg-red-500/20">
1229
+ CRITICAL
1230
+ </span>
1231
+ )}
1232
+
1233
+ {/* Overdue badge */}
1234
+ {isOverdue && (
1235
+ <span className="text-[9px] text-orange-400 font-semibold shrink-0">
1236
+ OVERDUE
1237
+ </span>
1238
+ )}
1239
+
1240
+ {/* Progress rollup */}
1241
+ {progress && (
1242
+ <span className="text-[9px] text-muted-foreground shrink-0 tabular-nums">
1243
+ {progress.done}/{progress.total}
1244
+ </span>
1245
+ )}
1246
+
1247
+ {/* Estimated hours */}
1248
+ {estimate && (
1249
+ <span className="inline-flex items-center gap-0.5 text-[9px] text-cyan-400 font-medium shrink-0 tabular-nums bg-cyan-500/10 rounded-full px-1.5 py-0.5" title="Estimated time">
1250
+ <Clock className="h-2.5 w-2.5" />
1251
+ {estimate}
1252
+ </span>
1253
+ )}
1254
+
1255
+ {/* Blocked-by tooltip */}
1256
+ {blockers && blockers.length > 0 && (
1257
+ <span className="relative group/tip shrink-0">
1258
+ <AlertTriangle className="h-3 w-3 text-red-400" />
1259
+ <span className="absolute hidden group-hover/tip:block left-0 top-full z-50 bg-popover border border-border rounded-md p-2 shadow-lg text-[10px] text-foreground whitespace-nowrap min-w-[160px]">
1260
+ <span className="font-semibold block mb-1">Blocked by:</span>
1261
+ {blockers.map((b) => (
1262
+ <span key={b.id} className="block text-muted-foreground">{b.id}: {b.title}</span>
1263
+ ))}
1264
+ </span>
1265
+ </span>
1266
+ )}
1267
+
1268
+ {/* Add child button */}
1269
+ {(node.type === "project" || node.type === "pipeline") && (
1270
+ <button
1271
+ onClick={(e) => {
1272
+ e.stopPropagation();
1273
+ const childType =
1274
+ node.type === "project" ? "pipeline" : "phase";
1275
+ setDialogType(childType);
1276
+ setEditItem(null);
1277
+ setDialogParentId(node.id);
1278
+ setDialogOpen(true);
1279
+ }}
1280
+ className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-foreground p-0.5 mr-1"
1281
+ >
1282
+ <Plus className="h-3 w-3" />
1283
+ </button>
1284
+ )}
1285
+
1286
+ {/* Dependency mode button */}
1287
+ <button
1288
+ onClick={(e) => {
1289
+ e.stopPropagation();
1290
+ setDepMode(depMode === node.id ? null : node.id);
1291
+ }}
1292
+ className={`opacity-0 group-hover:opacity-100 p-0.5 mr-1 transition-colors ${
1293
+ depMode === node.id
1294
+ ? "text-primary opacity-100"
1295
+ : "text-muted-foreground hover:text-foreground"
1296
+ }`}
1297
+ title="Link dependency"
1298
+ >
1299
+ <GitBranch className="h-3 w-3" />
1300
+ </button>
1301
+ </div>
1302
+ );
1303
+ })}
1304
+
1305
+ </div>
1306
+
1307
+ {/* Resize handle — flex sibling so it spans full panel height regardless of scroll */}
1308
+ <div
1309
+ className="shrink-0 w-1.5 cursor-col-resize hover:bg-primary/30 active:bg-primary/50 transition-colors z-10 border-r border-border"
1310
+ onMouseDown={handleResizeStart}
1311
+ >
1312
+ <div className="w-px h-full bg-border ml-auto" />
1313
+ </div>
1314
+
1315
+ {/* Right panel: Gantt timeline */}
1316
+ <div className="flex-1 overflow-auto" ref={ganttRef} onScroll={handleGanttScroll}>
1317
+ <div style={{ width: timelineWidth, minWidth: "100%" }}>
1318
+ {/* Timeline header */}
1319
+ <div className="border-b border-border flex flex-col relative bg-card/50 sticky top-0 z-10">
1320
+ {/* Month row (always visible) */}
1321
+ <div className="h-6 flex items-end relative">
1322
+ {months.map((month) => {
1323
+ const offset =
1324
+ differenceInDays(month, viewStart) * dayWidth;
1325
+ const monthEnd = endOfMonth(month);
1326
+ const daysInMonth = differenceInDays(monthEnd, month) + 1;
1327
+ const width = daysInMonth * dayWidth;
1328
+
1329
+ return (
1330
+ <div
1331
+ key={month.toISOString()}
1332
+ className="absolute border-l border-border/30 flex items-end pb-0.5 pl-2"
1333
+ style={{ left: offset, width }}
1334
+ >
1335
+ <span className="text-[10px] text-muted-foreground font-medium">
1336
+ {format(month, "MMM yyyy")}
1337
+ </span>
1338
+ </div>
1339
+ );
1340
+ })}
1341
+ </div>
1342
+
1343
+ {/* Day row (day/week zoom only) */}
1344
+ {(zoom === "day" || zoom === "week") && (
1345
+ <div className="h-5 flex items-end relative">
1346
+ {days.map((day) => {
1347
+ const offset = differenceInDays(day, viewStart) * dayWidth;
1348
+ const today = isToday(day);
1349
+ const weekend = isWeekend(day);
1350
+ return (
1351
+ <div
1352
+ key={day.toISOString()}
1353
+ className={`absolute border-l flex items-end pb-0.5 ${
1354
+ today
1355
+ ? "border-red-500/40 bg-red-500/10"
1356
+ : weekend
1357
+ ? "border-border/20 bg-accent/10"
1358
+ : "border-border/20"
1359
+ }`}
1360
+ style={{ left: offset, width: dayWidth }}
1361
+ >
1362
+ <span className={`text-[9px] px-1 truncate ${
1363
+ today ? "text-red-400 font-semibold" : weekend ? "text-muted-foreground/50" : "text-muted-foreground"
1364
+ }`}>
1365
+ {zoom === "day" ? format(day, "EEE d") : format(day, "d")}
1366
+ </span>
1367
+ </div>
1368
+ );
1369
+ })}
1370
+ </div>
1371
+ )}
1372
+ </div>
1373
+
1374
+ {/* Rows + Bars */}
1375
+ <div className="relative">
1376
+ {/* Grid background lines */}
1377
+ {(zoom === "day" || zoom === "week") ? (
1378
+ days.map((day) => {
1379
+ const offset = differenceInDays(day, viewStart) * dayWidth;
1380
+ const weekend = isWeekend(day);
1381
+ const today = isToday(day);
1382
+ return (
1383
+ <div
1384
+ key={`grid-${day.toISOString()}`}
1385
+ className={`absolute top-0 border-l ${
1386
+ today
1387
+ ? "border-red-500/20 bg-red-500/5"
1388
+ : weekend
1389
+ ? "border-border/10 bg-accent/5"
1390
+ : "border-border/10"
1391
+ }`}
1392
+ style={{ left: offset, width: dayWidth, height: filteredRows.length * ROW_HEIGHT }}
1393
+ />
1394
+ );
1395
+ })
1396
+ ) : (
1397
+ months.map((month) => {
1398
+ const offset =
1399
+ differenceInDays(month, viewStart) * dayWidth;
1400
+ return (
1401
+ <div
1402
+ key={`grid-${month.toISOString()}`}
1403
+ className="absolute top-0 bottom-0 border-l border-border/20"
1404
+ style={{ left: offset, height: filteredRows.length * ROW_HEIGHT }}
1405
+ />
1406
+ );
1407
+ })
1408
+ )}
1409
+
1410
+ {/* Today marker */}
1411
+ {todayOffset !== null && (
1412
+ <div
1413
+ className="absolute top-0 w-px bg-red-500/70 z-20"
1414
+ style={{
1415
+ left: todayOffset,
1416
+ height: filteredRows.length * ROW_HEIGHT,
1417
+ }}
1418
+ />
1419
+ )}
1420
+
1421
+ {/* Bars */}
1422
+ {filteredRows.map((node, rowIdx) => {
1423
+ const pos = getBarPosition(node);
1424
+ const nodeIsCritical = criticalPathSet.has(node.id);
1425
+ const nodeIsOverdue = !!(
1426
+ node.endDate &&
1427
+ isBefore(parseISO(node.endDate), todayDate) &&
1428
+ node.status !== "done" &&
1429
+ node.status !== "cancelled"
1430
+ );
1431
+ const barColor = nodeIsCritical
1432
+ ? "#ef4444"
1433
+ : node.color || STATUS_COLORS[node.status] || "#71717a";
1434
+
1435
+ return (
1436
+ <div
1437
+ key={node.id}
1438
+ data-gantt-id={node.id}
1439
+ className={`relative ${nodeIsCritical ? "z-[1]" : ""}`}
1440
+ style={{ height: ROW_HEIGHT }}
1441
+ >
1442
+ {/* Row background */}
1443
+ <div
1444
+ className={`absolute inset-0 ${
1445
+ nodeIsCritical
1446
+ ? "bg-red-500/15"
1447
+ : nodeIsOverdue
1448
+ ? "bg-orange-500/10"
1449
+ : rowIdx % 2 === 0
1450
+ ? "bg-transparent"
1451
+ : "bg-accent/5"
1452
+ }`}
1453
+ />
1454
+
1455
+ {/* Bar */}
1456
+ {pos && (
1457
+ <GanttBar
1458
+ id={node.id}
1459
+ left={pos.left}
1460
+ width={pos.width}
1461
+ color={barColor}
1462
+ label={node.title}
1463
+ type={node.type || "task"}
1464
+ status={node.status}
1465
+ onDragEnd={handleBarDrag}
1466
+ isCritical={nodeIsCritical}
1467
+ isOverdue={nodeIsOverdue}
1468
+ isMilestone={node.type === "milestone"}
1469
+ progress={progressMap.get(node.id) ?? null}
1470
+ onDoubleClick={() => {
1471
+ setEditItem(node);
1472
+ setDialogType(
1473
+ (node.type as "project" | "pipeline" | "phase") || "project"
1474
+ );
1475
+ setDialogParentId(node.parentId);
1476
+ setDialogOpen(true);
1477
+ }}
1478
+ />
1479
+ )}
1480
+ </div>
1481
+ );
1482
+ })}
1483
+
1484
+ {/* Dependency arrows (SVG overlay) */}
1485
+ <svg
1486
+ className="absolute top-0 left-0 pointer-events-none z-10"
1487
+ style={{
1488
+ width: timelineWidth,
1489
+ height: filteredRows.length * ROW_HEIGHT,
1490
+ }}
1491
+ >
1492
+ {dependencies.map((dep) => {
1493
+ const sourceIdx = filteredRows.findIndex(
1494
+ (r) => r.id === dep.sourceId
1495
+ );
1496
+ const targetIdx = filteredRows.findIndex(
1497
+ (r) => r.id === dep.targetId
1498
+ );
1499
+ if (sourceIdx === -1 || targetIdx === -1) return null;
1500
+
1501
+ const sourcePos = getBarPosition(filteredRows[sourceIdx]);
1502
+ const targetPos = getBarPosition(filteredRows[targetIdx]);
1503
+ if (!sourcePos || !targetPos) return null;
1504
+
1505
+ const x1 = sourcePos.left + sourcePos.width;
1506
+ const y1 = sourceIdx * ROW_HEIGHT + ROW_HEIGHT / 2;
1507
+ const x2 = targetPos.left;
1508
+ const y2 = targetIdx * ROW_HEIGHT + ROW_HEIGHT / 2;
1509
+
1510
+ const midX = x1 + (x2 - x1) / 2;
1511
+
1512
+ return (
1513
+ <g key={dep.id}>
1514
+ <path
1515
+ d={`M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`}
1516
+ fill="none"
1517
+ stroke="#6d28d9"
1518
+ strokeWidth={1.5}
1519
+ strokeDasharray="4 2"
1520
+ opacity={0.6}
1521
+ />
1522
+ {/* Arrow head */}
1523
+ <polygon
1524
+ points={`${x2},${y2} ${x2 - 6},${y2 - 3} ${x2 - 6},${y2 + 3}`}
1525
+ fill="#6d28d9"
1526
+ opacity={0.6}
1527
+ />
1528
+ </g>
1529
+ );
1530
+ })}
1531
+ </svg>
1532
+ </div>
1533
+ </div>
1534
+ </div>
1535
+ </div>
1536
+
1537
+ {/* Dependency mode indicator */}
1538
+ {depMode && (
1539
+ <div className="absolute bottom-4 left-1/2 -translate-x-1/2 bg-primary text-primary-foreground px-4 py-2 rounded-lg text-xs font-medium shadow-lg z-50 flex items-center gap-2">
1540
+ <GitBranch className="h-3.5 w-3.5" />
1541
+ Click a target to create dependency
1542
+ <button
1543
+ onClick={() => setDepMode(null)}
1544
+ className="ml-2 text-primary-foreground/70 hover:text-primary-foreground"
1545
+ >
1546
+ Cancel
1547
+ </button>
1548
+ </div>
1549
+ )}
1550
+
1551
+ {/* Dialog */}
1552
+ <UnifiedTaskDialog
1553
+ item={editItem}
1554
+ open={dialogOpen}
1555
+ onClose={() => {
1556
+ setDialogOpen(false);
1557
+ setEditItem(null);
1558
+ }}
1559
+ defaultType={dialogType}
1560
+ defaultParentId={dialogParentId}
1561
+ parentOptions={parentOptions}
1562
+ predecessors={editItem ? predecessorMap.get(editItem.id) ?? [] : []}
1563
+ successors={editItem ? successorMap.get(editItem.id) ?? [] : []}
1564
+ />
1565
+ </div>
1566
+ );
1567
+ }
1568
+
1569
+ // --- Gantt Bar sub-component with drag ---
1570
+
1571
+ interface GanttBarProps {
1572
+ id: string;
1573
+ left: number;
1574
+ width: number;
1575
+ color: string;
1576
+ label: string;
1577
+ type: string;
1578
+ status: string;
1579
+ onDragEnd: (id: string, newLeft: number, newWidth: number) => void;
1580
+ onDoubleClick?: () => void;
1581
+ isCritical?: boolean;
1582
+ isOverdue?: boolean;
1583
+ isMilestone?: boolean;
1584
+ progress?: { done: number; total: number } | null;
1585
+ }
1586
+
1587
+ function GanttBar({ id, left, width, color, label, type, status, onDragEnd, onDoubleClick, isCritical, isOverdue, isMilestone, progress }: GanttBarProps) {
1588
+ const barRef = useRef<HTMLDivElement>(null);
1589
+ const [dragging, setDragging] = useState<{
1590
+ mode: "move" | "resize-right";
1591
+ startX: number;
1592
+ origLeft: number;
1593
+ origWidth: number;
1594
+ } | null>(null);
1595
+ const [currentLeft, setCurrentLeft] = useState(left);
1596
+ const [currentWidth, setCurrentWidth] = useState(width);
1597
+
1598
+ useEffect(() => {
1599
+ if (!dragging) {
1600
+ setCurrentLeft(left);
1601
+ setCurrentWidth(width);
1602
+ }
1603
+ }, [left, width, dragging]);
1604
+
1605
+ useEffect(() => {
1606
+ if (!dragging) return;
1607
+
1608
+ const handleMove = (e: MouseEvent) => {
1609
+ const dx = e.clientX - dragging.startX;
1610
+ if (dragging.mode === "move") {
1611
+ setCurrentLeft(dragging.origLeft + dx);
1612
+ } else {
1613
+ setCurrentWidth(Math.max(dragging.origWidth + dx, 14));
1614
+ }
1615
+ };
1616
+
1617
+ const handleUp = (e: MouseEvent) => {
1618
+ const dx = e.clientX - dragging.startX;
1619
+ if (dragging.mode === "move") {
1620
+ onDragEnd(id, dragging.origLeft + dx, currentWidth);
1621
+ } else {
1622
+ onDragEnd(id, currentLeft, Math.max(dragging.origWidth + dx, 14));
1623
+ }
1624
+ setDragging(null);
1625
+ };
1626
+
1627
+ document.addEventListener("mousemove", handleMove);
1628
+ document.addEventListener("mouseup", handleUp);
1629
+ return () => {
1630
+ document.removeEventListener("mousemove", handleMove);
1631
+ document.removeEventListener("mouseup", handleUp);
1632
+ };
1633
+ }, [dragging, id, currentLeft, currentWidth, onDragEnd]);
1634
+
1635
+ const barHeight = isMilestone ? 16 : type === "project" ? 10 : type === "pipeline" ? 18 : type === "phase" ? 24 : 28;
1636
+ const topOffset = (ROW_HEIGHT - barHeight) / 2;
1637
+ const opacity = status === "done" || status === "cancelled" ? 0.5 : 1;
1638
+ const showLabelInside = !isMilestone && currentWidth > 80;
1639
+ const pct = progress ? Math.round((progress.done / progress.total) * 100) : 0;
1640
+
1641
+ // Milestone: render diamond marker
1642
+ if (isMilestone) {
1643
+ return (
1644
+ <div
1645
+ ref={barRef}
1646
+ className="absolute group cursor-pointer"
1647
+ style={{ left: currentLeft - 8, top: topOffset }}
1648
+ onDoubleClick={(e) => { e.stopPropagation(); onDoubleClick?.(); }}
1649
+ >
1650
+ <div
1651
+ className={`w-4 h-4 rotate-45 border-2 ${isCritical ? "ring-2 ring-red-500/50" : ""}`}
1652
+ style={{ backgroundColor: color, borderColor: isCritical ? "#ef4444" : color, opacity }}
1653
+ />
1654
+ <span
1655
+ className="absolute text-[10px] text-foreground/70 whitespace-nowrap font-medium pointer-events-none"
1656
+ style={{ left: 20, top: 0, maxWidth: 200 }}
1657
+ >
1658
+ {label}
1659
+ </span>
1660
+ </div>
1661
+ );
1662
+ }
1663
+
1664
+ // Border classes for critical path / overdue
1665
+ const ringClass = isCritical
1666
+ ? "ring-2 ring-red-400 shadow-[0_0_10px_rgba(239,68,68,0.6)]"
1667
+ : isOverdue
1668
+ ? "ring-2 ring-orange-500/80"
1669
+ : "";
1670
+ const barColor = isCritical ? "#ef4444" : color;
1671
+ const criticalOpacity = isCritical ? 1 : opacity;
1672
+
1673
+ return (
1674
+ <div
1675
+ ref={barRef}
1676
+ className={`absolute group cursor-grab active:cursor-grabbing ${isCritical ? "z-10" : ""}`}
1677
+ style={{
1678
+ left: currentLeft,
1679
+ width: showLabelInside ? currentWidth : undefined,
1680
+ top: topOffset,
1681
+ height: barHeight,
1682
+ }}
1683
+ >
1684
+ {/* Bar body */}
1685
+ <div
1686
+ className={`h-full rounded-sm relative overflow-hidden ${ringClass}`}
1687
+ style={{ backgroundColor: barColor, opacity: criticalOpacity, width: currentWidth, minWidth: 8 }}
1688
+ onMouseDown={(e) => {
1689
+ e.preventDefault();
1690
+ setDragging({
1691
+ mode: "move",
1692
+ startX: e.clientX,
1693
+ origLeft: currentLeft,
1694
+ origWidth: currentWidth,
1695
+ });
1696
+ }}
1697
+ onDoubleClick={(e) => {
1698
+ e.stopPropagation();
1699
+ onDoubleClick?.();
1700
+ }}
1701
+ >
1702
+ {/* Progress fill for parent bars */}
1703
+ {progress && progress.total > 0 && (
1704
+ <div
1705
+ className="absolute inset-y-0 left-0 bg-white/20 rounded-sm"
1706
+ style={{ width: `${pct}%` }}
1707
+ />
1708
+ )}
1709
+
1710
+ {/* Label inside bar — sticky: tracks horizontal scroll so text stays visible */}
1711
+ {showLabelInside && (
1712
+ <span
1713
+ className="absolute inset-0 flex items-center text-[10px] text-white/90 truncate font-medium z-[1]"
1714
+ style={{
1715
+ paddingLeft: `clamp(8px, calc(var(--gantt-scroll-left, 0px) - ${currentLeft}px + 8px), ${Math.max(currentWidth - 40, 8)}px)`,
1716
+ paddingRight: 8,
1717
+ }}
1718
+ >
1719
+ {label}
1720
+ {progress && (
1721
+ <span className="ml-auto text-white/60 text-[9px] tabular-nums shrink-0 pl-1">
1722
+ {pct}%
1723
+ </span>
1724
+ )}
1725
+ </span>
1726
+ )}
1727
+ </div>
1728
+
1729
+ {/* Label outside bar (when bar is too narrow) */}
1730
+ {!showLabelInside && (
1731
+ <span
1732
+ className="absolute text-[10px] text-foreground/70 truncate whitespace-nowrap font-medium pointer-events-none"
1733
+ style={{ left: currentWidth + 4, top: "50%", transform: "translateY(-50%)", maxWidth: 200 }}
1734
+ >
1735
+ {label}
1736
+ </span>
1737
+ )}
1738
+
1739
+ {/* Resize handle (right edge) */}
1740
+ <div
1741
+ className="absolute top-0 right-0 w-2 h-full cursor-ew-resize opacity-0 group-hover:opacity-100 transition-opacity"
1742
+ onMouseDown={(e) => {
1743
+ e.preventDefault();
1744
+ e.stopPropagation();
1745
+ setDragging({
1746
+ mode: "resize-right",
1747
+ startX: e.clientX,
1748
+ origLeft: currentLeft,
1749
+ origWidth: currentWidth,
1750
+ });
1751
+ }}
1752
+ >
1753
+ <div className="h-full w-0.5 bg-white/50 ml-auto" />
1754
+ </div>
1755
+ </div>
1756
+ );
1757
+ }