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.
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/bin/discord-read.js +228 -0
- package/bin/fleet-deploy.js +365 -0
- package/bin/lane-watchdog.js +232 -0
- package/bin/mesh-agent.js +714 -0
- package/bin/mesh-bridge.js +535 -0
- package/bin/mesh-deploy-listener.js +322 -0
- package/bin/mesh-deploy.js +1048 -0
- package/bin/mesh-health-publisher.js +247 -0
- package/bin/mesh-task-daemon.js +451 -0
- package/bin/mesh-tool-discord.js +293 -0
- package/bin/mesh.js +649 -0
- package/boot/manifest.yaml +187 -0
- package/cli.js +35 -0
- package/config/daemon.json.template +16 -0
- package/config/obsidian-sync.json.template +39 -0
- package/config/openclaw.json.template +124 -0
- package/config/transcript-sources.json.template +22 -0
- package/identity/AGENTS.md +201 -0
- package/identity/CLAUDE.md +64 -0
- package/identity/DELEGATION.md +304 -0
- package/identity/HEARTBEAT.md +163 -0
- package/identity/MEMORY_SPEC.md +368 -0
- package/identity/PRINCIPLES.md +81 -0
- package/identity/SOUL.md +48 -0
- package/identity/TOOLS.md +47 -0
- package/install.sh +895 -0
- package/lib/agent-activity.js +390 -0
- package/lib/kanban-io.js +352 -0
- package/lib/mesh-registry.js +194 -0
- package/lib/mesh-roles.js +13 -0
- package/lib/mesh-tasks.js +306 -0
- package/lib/nats-resolve.js +108 -0
- package/mission-control/README.md +36 -0
- package/mission-control/drizzle/0000_simple_silhouette.sql +62 -0
- package/mission-control/drizzle/meta/0000_snapshot.json +413 -0
- package/mission-control/drizzle/meta/_journal.json +13 -0
- package/mission-control/drizzle.config.ts +13 -0
- package/mission-control/eslint.config.mjs +18 -0
- package/mission-control/next.config.ts +7 -0
- package/mission-control/package-lock.json +10518 -0
- package/mission-control/package.json +49 -0
- package/mission-control/postcss.config.mjs +7 -0
- package/mission-control/public/file.svg +1 -0
- package/mission-control/public/globe.svg +1 -0
- package/mission-control/public/next.svg +1 -0
- package/mission-control/public/vercel.svg +1 -0
- package/mission-control/public/window.svg +1 -0
- package/mission-control/scripts/enrich-descriptions.js +193 -0
- package/mission-control/scripts/gen-chronology.js +102 -0
- package/mission-control/scripts/import-pipeline-v2.js +523 -0
- package/mission-control/scripts/import-pipeline.js +295 -0
- package/mission-control/src/app/api/activity/live/route.ts +27 -0
- package/mission-control/src/app/api/activity/route.ts +47 -0
- package/mission-control/src/app/api/burndown/route.ts +112 -0
- package/mission-control/src/app/api/critical-path/route.ts +159 -0
- package/mission-control/src/app/api/dependencies/route.ts +176 -0
- package/mission-control/src/app/api/memory/categories/route.ts +93 -0
- package/mission-control/src/app/api/memory/consolidate/route.ts +107 -0
- package/mission-control/src/app/api/memory/doc/route.ts +89 -0
- package/mission-control/src/app/api/memory/flush/route.ts +129 -0
- package/mission-control/src/app/api/memory/graph/route.ts +105 -0
- package/mission-control/src/app/api/memory/items/route.ts +86 -0
- package/mission-control/src/app/api/memory/list/route.ts +48 -0
- package/mission-control/src/app/api/memory/retrieve/route.ts +51 -0
- package/mission-control/src/app/api/memory/search/route.ts +143 -0
- package/mission-control/src/app/api/memory/sync/route.ts +23 -0
- package/mission-control/src/app/api/memory/wikilinks/route.ts +75 -0
- package/mission-control/src/app/api/mesh/events/route.ts +67 -0
- package/mission-control/src/app/api/mesh/nodes/route.ts +221 -0
- package/mission-control/src/app/api/mesh/tokens/route.ts +133 -0
- package/mission-control/src/app/api/projects/route.ts +102 -0
- package/mission-control/src/app/api/resolve-path/route.ts +92 -0
- package/mission-control/src/app/api/scheduler/tick/route.ts +38 -0
- package/mission-control/src/app/api/scheduler/waves/route.ts +54 -0
- package/mission-control/src/app/api/screenshot/route.ts +127 -0
- package/mission-control/src/app/api/settings/gateway/route.ts +92 -0
- package/mission-control/src/app/api/skills/[id]/health/route.ts +57 -0
- package/mission-control/src/app/api/skills/list/route.ts +41 -0
- package/mission-control/src/app/api/souls/[id]/evolution/route.ts +253 -0
- package/mission-control/src/app/api/souls/[id]/prompt/route.ts +205 -0
- package/mission-control/src/app/api/souls/[id]/propagate/route.ts +146 -0
- package/mission-control/src/app/api/souls/route.ts +174 -0
- package/mission-control/src/app/api/tasks/[id]/handoff/route.ts +115 -0
- package/mission-control/src/app/api/tasks/[id]/route.ts +266 -0
- package/mission-control/src/app/api/tasks/[id]/tree/route.ts +94 -0
- package/mission-control/src/app/api/tasks/route.ts +253 -0
- package/mission-control/src/app/api/tts/route.ts +47 -0
- package/mission-control/src/app/api/workspace/files/route.ts +88 -0
- package/mission-control/src/app/api/workspace/read/route.ts +73 -0
- package/mission-control/src/app/burndown/page.tsx +309 -0
- package/mission-control/src/app/calendar/page.tsx +839 -0
- package/mission-control/src/app/favicon.ico +0 -0
- package/mission-control/src/app/globals.css +67 -0
- package/mission-control/src/app/graph/page.tsx +352 -0
- package/mission-control/src/app/layout.tsx +35 -0
- package/mission-control/src/app/live/page.tsx +232 -0
- package/mission-control/src/app/memory/page.tsx +154 -0
- package/mission-control/src/app/mesh/page.tsx +457 -0
- package/mission-control/src/app/obsidian/page.tsx +252 -0
- package/mission-control/src/app/page.tsx +70 -0
- package/mission-control/src/app/roadmap/page.tsx +1757 -0
- package/mission-control/src/app/settings/page.tsx +260 -0
- package/mission-control/src/app/souls/page.tsx +573 -0
- package/mission-control/src/components/board/activity-timeline.tsx +96 -0
- package/mission-control/src/components/board/daily-board.tsx +373 -0
- package/mission-control/src/components/board/kanban-board.tsx +364 -0
- package/mission-control/src/components/board/kanban-column.tsx +105 -0
- package/mission-control/src/components/board/live-stream.tsx +116 -0
- package/mission-control/src/components/board/skill-health-card.tsx +128 -0
- package/mission-control/src/components/board/status-banner.tsx +124 -0
- package/mission-control/src/components/board/task-card.tsx +454 -0
- package/mission-control/src/components/board/unified-task-dialog.tsx +1043 -0
- package/mission-control/src/components/layout/resizable-layout.tsx +68 -0
- package/mission-control/src/components/layout/sidebar.tsx +90 -0
- package/mission-control/src/components/live/audio-spectrum.tsx +106 -0
- package/mission-control/src/components/live/chat-bubble.tsx +52 -0
- package/mission-control/src/components/live/chat-input.tsx +92 -0
- package/mission-control/src/components/memory/doc-reader.tsx +172 -0
- package/mission-control/src/components/memory/memory-list.tsx +169 -0
- package/mission-control/src/components/memory/search-bar.tsx +67 -0
- package/mission-control/src/components/memory/search-results.tsx +149 -0
- package/mission-control/src/components/obsidian/backlinks-panel.tsx +52 -0
- package/mission-control/src/components/obsidian/file-tree.tsx +186 -0
- package/mission-control/src/components/obsidian/local-graph.tsx +107 -0
- package/mission-control/src/components/obsidian/obsidian-graph.tsx +192 -0
- package/mission-control/src/components/obsidian/obsidian-reader.tsx +246 -0
- package/mission-control/src/lib/activity.ts +29 -0
- package/mission-control/src/lib/config.ts +21 -0
- package/mission-control/src/lib/db/index.ts +429 -0
- package/mission-control/src/lib/db/schema.ts +218 -0
- package/mission-control/src/lib/gateway-notify.ts +113 -0
- package/mission-control/src/lib/hooks.ts +536 -0
- package/mission-control/src/lib/memory/categories.ts +125 -0
- package/mission-control/src/lib/memory/entities.ts +482 -0
- package/mission-control/src/lib/memory/extract.ts +369 -0
- package/mission-control/src/lib/memory/retrieval.ts +281 -0
- package/mission-control/src/lib/memory/wikilinks.ts +147 -0
- package/mission-control/src/lib/nats.ts +126 -0
- package/mission-control/src/lib/parsers/clawvault-doc.ts +98 -0
- package/mission-control/src/lib/parsers/daily-log.ts +73 -0
- package/mission-control/src/lib/parsers/memory-md.ts +81 -0
- package/mission-control/src/lib/parsers/task-markdown.ts +459 -0
- package/mission-control/src/lib/parsers/transcript.ts +209 -0
- package/mission-control/src/lib/scheduler.ts +394 -0
- package/mission-control/src/lib/speech/use-speech-pipeline.ts +176 -0
- package/mission-control/src/lib/sync/memory.ts +224 -0
- package/mission-control/src/lib/sync/tasks.ts +271 -0
- package/mission-control/src/lib/tts/edge.ts +31 -0
- package/mission-control/src/lib/tts/google.ts +78 -0
- package/mission-control/src/lib/tts/index.ts +39 -0
- package/mission-control/src/lib/tts/types.ts +18 -0
- package/mission-control/tsconfig.json +42 -0
- package/obsidian-vault/.obsidian/app.json +10 -0
- package/obsidian-vault/.obsidian/community-plugins.json +8 -0
- package/obsidian-vault/.obsidian/graph.json +40 -0
- package/obsidian-vault/.obsidian/plugins/obsidian-local-rest-api/main.js +58769 -0
- package/obsidian-vault/.obsidian/plugins/obsidian-local-rest-api/manifest.json +10 -0
- package/obsidian-vault/.obsidian/plugins/obsidian-local-rest-api/styles.css +47 -0
- package/obsidian-vault/00-meta/.gitkeep +0 -0
- package/obsidian-vault/01-architecture/.gitkeep +0 -0
- package/obsidian-vault/02-smart-contracts/.gitkeep +0 -0
- package/obsidian-vault/03-backend/.gitkeep +0 -0
- package/obsidian-vault/04-mobile/.gitkeep +0 -0
- package/obsidian-vault/05-ar-mapping/.gitkeep +0 -0
- package/obsidian-vault/06-3d-assets/.gitkeep +0 -0
- package/obsidian-vault/07-sound-music/.gitkeep +0 -0
- package/obsidian-vault/08-lore/.gitkeep +0 -0
- package/obsidian-vault/09-quests-playthrough/.gitkeep +0 -0
- package/obsidian-vault/10-economy/.gitkeep +0 -0
- package/obsidian-vault/11-nft-assets/.gitkeep +0 -0
- package/obsidian-vault/12-nft-mechanics/.gitkeep +0 -0
- package/obsidian-vault/13-dao-guild-social/.gitkeep +0 -0
- package/obsidian-vault/14-game-progression/.gitkeep +0 -0
- package/obsidian-vault/15-analytics/.gitkeep +0 -0
- package/obsidian-vault/16-security/.gitkeep +0 -0
- package/obsidian-vault/17-devops/.gitkeep +0 -0
- package/obsidian-vault/18-marketplace/.gitkeep +0 -0
- package/obsidian-vault/19-decisions/.gitkeep +0 -0
- package/obsidian-vault/20-business-strategy/.gitkeep +0 -0
- package/obsidian-vault/21-legal-regulatory/.gitkeep +0 -0
- package/obsidian-vault/nodes/.gitkeep +0 -0
- package/openclaw.env.example +17 -0
- package/package.json +45 -0
- package/services/launchd/ai.openclaw.gateway.plist +59 -0
- package/services/launchd/ai.openclaw.lane-watchdog.plist +32 -0
- package/services/launchd/ai.openclaw.log-rotate.plist +28 -0
- package/services/launchd/ai.openclaw.memory-daemon.plist +36 -0
- package/services/launchd/ai.openclaw.mesh-agent.plist +38 -0
- package/services/launchd/ai.openclaw.mesh-bridge.plist +36 -0
- package/services/launchd/ai.openclaw.mesh-deploy-listener.plist +33 -0
- package/services/launchd/ai.openclaw.mesh-health-publisher.plist +29 -0
- package/services/launchd/ai.openclaw.mesh-task-daemon.plist +36 -0
- package/services/launchd/ai.openclaw.mesh-tool-discord.plist +36 -0
- package/services/launchd/ai.openclaw.mission-control.plist +41 -0
- package/services/service-manifest.json +13 -0
- package/services/systemd/openclaw-gateway.service +21 -0
- package/services/systemd/openclaw-lane-watchdog.service +21 -0
- package/services/systemd/openclaw-log-rotate.service +13 -0
- package/services/systemd/openclaw-log-rotate.timer +9 -0
- package/services/systemd/openclaw-memory-daemon.service +21 -0
- package/services/systemd/openclaw-mesh-agent.service +19 -0
- package/services/systemd/openclaw-mesh-bridge.service +21 -0
- package/services/systemd/openclaw-mesh-deploy-listener.service +23 -0
- package/services/systemd/openclaw-mesh-health-publisher.service +21 -0
- package/services/systemd/openclaw-mesh-task-daemon.service +21 -0
- package/services/systemd/openclaw-mesh-tool-discord.service +21 -0
- package/services/systemd/openclaw-mission-control.service +22 -0
- package/skills/1password/.clawhub/origin.json +7 -0
- package/skills/1password/SKILL.md +63 -0
- package/skills/1password/references/cli-examples.md +29 -0
- package/skills/1password/references/get-started.md +17 -0
- package/skills/acquisition-channel-advisor/SKILL.md +643 -0
- package/skills/acquisition-channel-advisor/examples/conversation-flow.md +531 -0
- package/skills/agent-browser/.clawhub/origin.json +7 -0
- package/skills/agent-browser/CONTRIBUTING.md +63 -0
- package/skills/agent-browser/SKILL.md +338 -0
- package/skills/agentic-compass/.clawhub/origin.json +7 -0
- package/skills/agentic-compass/README.md +96 -0
- package/skills/agentic-compass/SKILL.md +112 -0
- package/skills/agentic-compass/references/README.md +5 -0
- package/skills/agentic-compass/scripts/agentic-compass.py +196 -0
- package/skills/arcane-dev-ops/SKILL.md +61 -0
- package/skills/arcane-dev-ops/references/checklist.md +22 -0
- package/skills/arcane-dev-ops/references/validation-cases.md +11 -0
- package/skills/arcane-dev-ops/scripts/prepush_check.sh +41 -0
- package/skills/auto-updater/.clawhub/origin.json +7 -0
- package/skills/auto-updater/SKILL.md +158 -0
- package/skills/auto-updater/references/agent-guide.md +152 -0
- package/skills/auto-updater/references/summary-examples.md +109 -0
- package/skills/business-health-diagnostic/SKILL.md +782 -0
- package/skills/byterover/.clawhub/origin.json +7 -0
- package/skills/byterover/SKILL.md +105 -0
- package/skills/byterover/TROUBLESHOOTING.md +50 -0
- package/skills/byterover/WORKFLOWS.md +229 -0
- package/skills/capability-evolver/.clawhub/origin.json +7 -0
- package/skills/capability-evolver/CONTRIBUTING.md +11 -0
- package/skills/capability-evolver/README.md +157 -0
- package/skills/capability-evolver/README.zh-CN.md +112 -0
- package/skills/capability-evolver/SKILL.md +93 -0
- package/skills/capability-evolver/assets/gep/capsules.json +5 -0
- package/skills/capability-evolver/assets/gep/genes.json +104 -0
- package/skills/capability-evolver/index.js +59 -0
- package/skills/capability-evolver/package.json +22 -0
- package/skills/capability-evolver/scripts/analyze_by_skill.js +121 -0
- package/skills/capability-evolver/scripts/build_public.js +350 -0
- package/skills/capability-evolver/scripts/export_history.js +98 -0
- package/skills/capability-evolver/scripts/extract_log.js +85 -0
- package/skills/capability-evolver/scripts/generate_history.js +75 -0
- package/skills/capability-evolver/scripts/human_report.js +147 -0
- package/skills/capability-evolver/scripts/publish_public.js +516 -0
- package/skills/capability-evolver/scripts/suggest_version.js +89 -0
- package/skills/capability-evolver/src/evolve.js +594 -0
- package/skills/capability-evolver/src/gep/assetStore.js +204 -0
- package/skills/capability-evolver/src/gep/candidates.js +134 -0
- package/skills/capability-evolver/src/gep/paths.js +23 -0
- package/skills/capability-evolver/src/gep/prompt.js +254 -0
- package/skills/capability-evolver/src/gep/selector.js +89 -0
- package/skills/capability-evolver/src/gep/signals.js +27 -0
- package/skills/cc-godmode/.clawhub/origin.json +7 -0
- package/skills/cc-godmode/CHANGELOG.md +66 -0
- package/skills/cc-godmode/README.md +293 -0
- package/skills/cc-godmode/SKILL.md +242 -0
- package/skills/cc-godmode/docs/AGENTS.md +332 -0
- package/skills/cc-godmode/docs/MIGRATION.md +206 -0
- package/skills/cc-godmode/docs/TROUBLESHOOTING.md +357 -0
- package/skills/cc-godmode/docs/WORKFLOWS.md +329 -0
- package/skills/cc-godmode/references/agents.md +433 -0
- package/skills/cc-godmode/scripts/build-skill.js +232 -0
- package/skills/clawdbot-filesystem/.clawhub/origin.json +7 -0
- package/skills/clawdbot-filesystem/LICENSE.md +21 -0
- package/skills/clawdbot-filesystem/README.md +322 -0
- package/skills/clawdbot-filesystem/SKILL.md +219 -0
- package/skills/clawdbot-filesystem/config.json +41 -0
- package/skills/clawdbot-filesystem/package.json +69 -0
- package/skills/clawdbot-security-check/.clawhub/origin.json +7 -0
- package/skills/clawdbot-security-check/README.md +168 -0
- package/skills/clawdbot-security-check/SKILL.md +145 -0
- package/skills/clawdbot-security-check/references/audit-checks.md +521 -0
- package/skills/clawdbot-security-check/skill.json +42 -0
- package/skills/clawddocs/.clawhub/origin.json +7 -0
- package/skills/clawddocs/SKILL.md +176 -0
- package/skills/clawddocs/package.json +9 -0
- package/skills/clawddocs/scripts/build-index.sh +17 -0
- package/skills/clawddocs/scripts/cache.sh +13 -0
- package/skills/clawddocs/scripts/fetch-doc.sh +7 -0
- package/skills/clawddocs/scripts/recent.sh +5 -0
- package/skills/clawddocs/scripts/search.sh +8 -0
- package/skills/clawddocs/scripts/sitemap.sh +23 -0
- package/skills/clawddocs/scripts/track-changes.sh +16 -0
- package/skills/clawddocs/snippets/common-configs.md +69 -0
- package/skills/clawguard/.clawhub/origin.json +7 -0
- package/skills/clawguard/SKILL.md +137 -0
- package/skills/company-research/SKILL.md +393 -0
- package/skills/company-research/examples/sample.md +164 -0
- package/skills/company-research/template.md +60 -0
- package/skills/crypto-price/.clawhub/origin.json +7 -0
- package/skills/crypto-price/ARCHITECTURE.md +437 -0
- package/skills/crypto-price/README.md +194 -0
- package/skills/crypto-price/SKILL.md +61 -0
- package/skills/crypto-price/requirements.txt +1 -0
- package/skills/crypto-price/scripts/get_price_chart.py +988 -0
- package/skills/customer-journey-map/SKILL.md +343 -0
- package/skills/customer-journey-map/examples/sample.md +33 -0
- package/skills/customer-journey-map/template.md +28 -0
- package/skills/customer-journey-mapping-workshop/SKILL.md +522 -0
- package/skills/deep-research/.clawhub/origin.json +7 -0
- package/skills/deep-research/SKILL.md +93 -0
- package/skills/deep-research/rules/logic.md +32 -0
- package/skills/discord-telegram-triage/SKILL.md +59 -0
- package/skills/discord-telegram-triage/references/discord-runbook.md +28 -0
- package/skills/discord-telegram-triage/references/validation-cases.md +11 -0
- package/skills/discord-telegram-triage/scripts/triage_snapshot.sh +23 -0
- package/skills/discovery-interview-prep/SKILL.md +408 -0
- package/skills/discovery-process/SKILL.md +503 -0
- package/skills/discovery-process/examples/sample.md +60 -0
- package/skills/discovery-process/template.md +39 -0
- package/skills/dist/arcane-dev-ops.skill +0 -0
- package/skills/dist/discord-telegram-triage.skill +0 -0
- package/skills/dist/founder-brief-summarizer.skill +0 -0
- package/skills/epic-breakdown-advisor/SKILL.md +664 -0
- package/skills/epic-hypothesis/SKILL.md +285 -0
- package/skills/epic-hypothesis/examples/sample.md +104 -0
- package/skills/epic-hypothesis/template.md +30 -0
- package/skills/excel/.clawhub/origin.json +7 -0
- package/skills/excel/SKILL.md +332 -0
- package/skills/excel/scripts/excel.py +1120 -0
- package/skills/fast-browser-use/.clawhub/origin.json +7 -0
- package/skills/fast-browser-use/CODEBUDDY.md +142 -0
- package/skills/fast-browser-use/Cargo.toml +77 -0
- package/skills/fast-browser-use/README.md +62 -0
- package/skills/fast-browser-use/SKILL.md +217 -0
- package/skills/fast-browser-use/package-lock.json +28 -0
- package/skills/fast-browser-use/package.json +8 -0
- package/skills/fast-browser-use/rustfmt.toml +10 -0
- package/skills/fast-browser-use/src/bin/cli.rs +373 -0
- package/skills/fast-browser-use/src/bin/mcp_server.rs +203 -0
- package/skills/fast-browser-use/src/browser/config.rs +136 -0
- package/skills/fast-browser-use/src/browser/debug.rs +16 -0
- package/skills/fast-browser-use/src/browser/mod.rs +61 -0
- package/skills/fast-browser-use/src/browser/session.rs +478 -0
- package/skills/fast-browser-use/src/dom/element.rs +442 -0
- package/skills/fast-browser-use/src/dom/extract_dom.js +849 -0
- package/skills/fast-browser-use/src/dom/mod.rs +14 -0
- package/skills/fast-browser-use/src/dom/tree.rs +296 -0
- package/skills/fast-browser-use/src/dom/yaml.rs +149 -0
- package/skills/fast-browser-use/src/error.rs +115 -0
- package/skills/fast-browser-use/src/lib.rs +17 -0
- package/skills/fast-browser-use/src/mcp/handler.rs +63 -0
- package/skills/fast-browser-use/src/mcp/mod.rs +81 -0
- package/skills/fast-browser-use/src/tools/Readability.min.js +1480 -0
- package/skills/fast-browser-use/src/tools/annotate.rs +165 -0
- package/skills/fast-browser-use/src/tools/click.rs +84 -0
- package/skills/fast-browser-use/src/tools/close.rs +35 -0
- package/skills/fast-browser-use/src/tools/close_tab.rs +45 -0
- package/skills/fast-browser-use/src/tools/convert_to_markdown.js +117 -0
- package/skills/fast-browser-use/src/tools/cookies.rs +58 -0
- package/skills/fast-browser-use/src/tools/debug.rs +44 -0
- package/skills/fast-browser-use/src/tools/evaluate.rs +40 -0
- package/skills/fast-browser-use/src/tools/extract.rs +66 -0
- package/skills/fast-browser-use/src/tools/go_back.rs +35 -0
- package/skills/fast-browser-use/src/tools/go_forward.rs +35 -0
- package/skills/fast-browser-use/src/tools/hover.js +33 -0
- package/skills/fast-browser-use/src/tools/hover.rs +97 -0
- package/skills/fast-browser-use/src/tools/html_to_markdown.rs +99 -0
- package/skills/fast-browser-use/src/tools/input.rs +93 -0
- package/skills/fast-browser-use/src/tools/local_storage.rs +159 -0
- package/skills/fast-browser-use/src/tools/markdown.rs +181 -0
- package/skills/fast-browser-use/src/tools/mod.rs +326 -0
- package/skills/fast-browser-use/src/tools/navigate.rs +55 -0
- package/skills/fast-browser-use/src/tools/new_tab.rs +60 -0
- package/skills/fast-browser-use/src/tools/press_key.rs +78 -0
- package/skills/fast-browser-use/src/tools/read_links.rs +59 -0
- package/skills/fast-browser-use/src/tools/readability_script.rs +8 -0
- package/skills/fast-browser-use/src/tools/screenshot.rs +47 -0
- package/skills/fast-browser-use/src/tools/scroll.js +22 -0
- package/skills/fast-browser-use/src/tools/scroll.rs +95 -0
- package/skills/fast-browser-use/src/tools/select.js +23 -0
- package/skills/fast-browser-use/src/tools/select.rs +129 -0
- package/skills/fast-browser-use/src/tools/sitemap.rs +426 -0
- package/skills/fast-browser-use/src/tools/snapshot.rs +324 -0
- package/skills/fast-browser-use/src/tools/switch_tab.rs +69 -0
- package/skills/fast-browser-use/src/tools/tab_list.rs +76 -0
- package/skills/fast-browser-use/src/tools/utils.rs +92 -0
- package/skills/fast-browser-use/src/tools/wait.rs +53 -0
- package/skills/fast-browser-use/test_auth.json +3 -0
- package/skills/fast-browser-use/test_state.json +6 -0
- package/skills/fast-browser-use/tests/browser_tools_integration.rs +233 -0
- package/skills/fast-browser-use/tests/cli_recipes_integration.rs +112 -0
- package/skills/fast-browser-use/tests/cookies_integration.rs +56 -0
- package/skills/fast-browser-use/tests/debug_integration.rs +83 -0
- package/skills/fast-browser-use/tests/dom_integration.rs +170 -0
- package/skills/fast-browser-use/tests/local_storage_integration.rs +75 -0
- package/skills/fast-browser-use/tests/markdown_integration.rs +448 -0
- package/skills/fast-browser-use/tests/navigation_integration.rs +241 -0
- package/skills/fast-browser-use/tests/sitemap_integration.rs +326 -0
- package/skills/fast-browser-use/tests/tab_management_integration.rs +300 -0
- package/skills/feature-investment-advisor/SKILL.md +639 -0
- package/skills/feature-investment-advisor/examples/conversation-flow.md +538 -0
- package/skills/finance-based-pricing-advisor/SKILL.md +763 -0
- package/skills/finance-metrics-quickref/SKILL.md +309 -0
- package/skills/find-skills/.clawhub/origin.json +7 -0
- package/skills/find-skills/SKILL.md +143 -0
- package/skills/flavor-text-writer/SKILL.md +27 -0
- package/skills/founder-brief-summarizer/SKILL.md +52 -0
- package/skills/founder-brief-summarizer/references/response-templates.md +15 -0
- package/skills/founder-brief-summarizer/references/validation-cases.md +11 -0
- package/skills/founder-brief-summarizer/scripts/brief_template.sh +28 -0
- package/skills/frontend-design/.clawhub/origin.json +7 -0
- package/skills/frontend-design/LICENSE.txt +190 -0
- package/skills/frontend-design/SKILL.md +53 -0
- package/skills/gemini/.clawhub/origin.json +7 -0
- package/skills/gemini/SKILL.md +33 -0
- package/skills/gemini-deep-research/.clawhub/origin.json +7 -0
- package/skills/gemini-deep-research/SKILL.md +78 -0
- package/skills/gemini-deep-research/scripts/deep_research.py +176 -0
- package/skills/git-essentials/.clawhub/origin.json +7 -0
- package/skills/git-essentials/SKILL.md +239 -0
- package/skills/git-essentials/references/advanced.md +211 -0
- package/skills/github/.clawhub/origin.json +7 -0
- package/skills/github/SKILL.md +57 -0
- package/skills/google-drive/.clawhub/origin.json +7 -0
- package/skills/google-drive/LICENSE.txt +21 -0
- package/skills/google-drive/SKILL.md +320 -0
- package/skills/growth-loop/SKILL.md +270 -0
- package/skills/growth-loop/_meta.json +9 -0
- package/skills/growth-loop/references/diagnosis-framework.md +84 -0
- package/skills/growth-loop/references/platform-benchmarks.md +79 -0
- package/skills/growth-loop/scripts/init-campaign.sh +274 -0
- package/skills/humanize-ai-text/.clawhub/origin.json +7 -0
- package/skills/humanize-ai-text/SKILL.md +192 -0
- package/skills/humanize-ai-text/scripts/compare.py +58 -0
- package/skills/humanize-ai-text/scripts/detect.py +160 -0
- package/skills/humanize-ai-text/scripts/patterns.json +191 -0
- package/skills/humanize-ai-text/scripts/transform.py +127 -0
- package/skills/humanizer/.clawhub/origin.json +7 -0
- package/skills/humanizer/README.md +82 -0
- package/skills/humanizer/SKILL.md +443 -0
- package/skills/jobs-to-be-done/SKILL.md +378 -0
- package/skills/jobs-to-be-done/examples/sample.md +80 -0
- package/skills/jobs-to-be-done/template.md +65 -0
- package/skills/lean-ux-canvas/SKILL.md +561 -0
- package/skills/lean-ux-canvas/examples/sample.md +88 -0
- package/skills/lean-ux-canvas/template.md +32 -0
- package/skills/markdown-formatter/.clawhub/origin.json +7 -0
- package/skills/markdown-formatter/README.md +137 -0
- package/skills/markdown-formatter/SKILL.md +369 -0
- package/skills/markdown-formatter/config.json +20 -0
- package/skills/markdown-formatter/index.js +439 -0
- package/skills/markdown-formatter/package.json +23 -0
- package/skills/markdown-formatter/test.js +23 -0
- package/skills/marketing-mode/.clawhub/origin.json +7 -0
- package/skills/marketing-mode/README.md +49 -0
- package/skills/marketing-mode/SKILL.md +703 -0
- package/skills/marketing-mode/mode-prompt.md +39 -0
- package/skills/marketing-mode/skill.json +51 -0
- package/skills/memory-hygiene/.clawhub/origin.json +7 -0
- package/skills/memory-hygiene/SKILL.md +91 -0
- package/skills/memory-setup/.clawhub/origin.json +7 -0
- package/skills/memory-setup/SKILL.md +180 -0
- package/skills/memorylayer/.clawhub/origin.json +7 -0
- package/skills/memorylayer/README.md +197 -0
- package/skills/memorylayer/SKILL.md +227 -0
- package/skills/memorylayer/examples/agent-integration.js +145 -0
- package/skills/memorylayer/examples/basic-usage.js +87 -0
- package/skills/memorylayer/examples/token-savings-demo.js +183 -0
- package/skills/memorylayer/index.js +115 -0
- package/skills/memorylayer/package-lock.json +295 -0
- package/skills/memorylayer/package.json +27 -0
- package/skills/memorylayer/python/memorylayer_skill.py +230 -0
- package/skills/memorylayer/python/requirements.txt +7 -0
- package/skills/mesh/SKILL.md +184 -0
- package/skills/model-usage/.clawhub/origin.json +7 -0
- package/skills/model-usage/SKILL.md +54 -0
- package/skills/model-usage/references/codexbar-cli.md +28 -0
- package/skills/model-usage/scripts/model_usage.py +310 -0
- package/skills/moltbook-interact/.clawhub/origin.json +7 -0
- package/skills/moltbook-interact/INSTALL.md +139 -0
- package/skills/moltbook-interact/README.md +198 -0
- package/skills/moltbook-interact/SKILL.md +72 -0
- package/skills/moltbook-interact/references/api.md +106 -0
- package/skills/moltbook-interact/scripts/moltbook.sh +142 -0
- package/skills/moltbook-registry/.clawhub/origin.json +7 -0
- package/skills/moltbook-registry/README.md +26 -0
- package/skills/moltbook-registry/SKILL.md +82 -0
- package/skills/moltbook-registry/index.js +180 -0
- package/skills/moltbook-registry/package.json +11 -0
- package/skills/mythril-scanner/SKILL.md +27 -0
- package/skills/n8n/.clawhub/origin.json +7 -0
- package/skills/n8n/SKILL.md +141 -0
- package/skills/n8n/references/api.md +156 -0
- package/skills/n8n/scripts/n8n_api.py +158 -0
- package/skills/n8n-workflow-automation/.clawhub/origin.json +7 -0
- package/skills/n8n-workflow-automation/SKILL.md +103 -0
- package/skills/n8n-workflow-automation/assets/runbook-template.md +32 -0
- package/skills/narrative-designer/SKILL.md +27 -0
- package/skills/ontology/.clawhub/origin.json +7 -0
- package/skills/ontology/SKILL.md +236 -0
- package/skills/ontology/references/queries.md +211 -0
- package/skills/ontology/references/schema.md +322 -0
- package/skills/ontology/scripts/ontology.py +374 -0
- package/skills/openai-image-gen/.clawhub/origin.json +7 -0
- package/skills/openai-image-gen/SKILL.md +45 -0
- package/skills/openai-image-gen/scripts/gen.py +227 -0
- package/skills/openclaw-agent-optimize/.clawhub/origin.json +7 -0
- package/skills/openclaw-agent-optimize/SKILL.md +33 -0
- package/skills/openclaw-agent-optimize/references/agent-orchestration.md +20 -0
- package/skills/openclaw-agent-optimize/references/context-management.md +15 -0
- package/skills/openclaw-agent-optimize/references/continuous-learning.md +14 -0
- package/skills/openclaw-agent-optimize/references/cron-optimization.md +16 -0
- package/skills/openclaw-agent-optimize/references/memory-patterns.md +14 -0
- package/skills/openclaw-agent-optimize/references/model-selection.md +18 -0
- package/skills/openclaw-skill-scanner/.clawhub/origin.json +7 -0
- package/skills/openclaw-skill-scanner/SKILL.md +88 -0
- package/skills/openclaw-skill-scanner/install-hook.sh +294 -0
- package/skills/openclaw-skill-scanner/report-template.md +53 -0
- package/skills/openclaw-skill-scanner/scanner.py +929 -0
- package/skills/openclaw-skill-scanner/whitelist.json +18 -0
- package/skills/opportunity-solution-tree/SKILL.md +428 -0
- package/skills/opportunity-solution-tree/examples/sample.md +104 -0
- package/skills/opportunity-solution-tree/template.md +33 -0
- package/skills/pdf/.clawhub/origin.json +7 -0
- package/skills/pdf/SKILL.md +304 -0
- package/skills/pestel-analysis/SKILL.md +384 -0
- package/skills/pestel-analysis/examples/sample.md +143 -0
- package/skills/pestel-analysis/template.md +53 -0
- package/skills/pol-probe/SKILL.md +217 -0
- package/skills/pol-probe/examples/sample.md +136 -0
- package/skills/pol-probe/template.md +59 -0
- package/skills/pol-probe-advisor/SKILL.md +492 -0
- package/skills/positioning-statement/SKILL.md +229 -0
- package/skills/positioning-statement/examples/sample.md +51 -0
- package/skills/positioning-statement/template.md +25 -0
- package/skills/positioning-workshop/SKILL.md +424 -0
- package/skills/prd-development/SKILL.md +654 -0
- package/skills/prd-development/examples/sample.md +43 -0
- package/skills/prd-development/template.md +55 -0
- package/skills/press-release/SKILL.md +277 -0
- package/skills/press-release/examples/sample.md +73 -0
- package/skills/press-release/template.md +39 -0
- package/skills/prioritization-advisor/SKILL.md +451 -0
- package/skills/proactive-messages/.clawhub/origin.json +7 -0
- package/skills/proactive-messages/SKILL.md +91 -0
- package/skills/problem-framing-canvas/SKILL.md +466 -0
- package/skills/problem-framing-canvas/examples/sample.md +58 -0
- package/skills/problem-framing-canvas/template.md +22 -0
- package/skills/problem-statement/SKILL.md +255 -0
- package/skills/problem-statement/examples/sample.md +82 -0
- package/skills/problem-statement/template.md +37 -0
- package/skills/product-strategy-session/SKILL.md +434 -0
- package/skills/product-strategy-session/examples/sample.md +67 -0
- package/skills/product-strategy-session/template.md +38 -0
- package/skills/prompt-guard/.clawhub/origin.json +7 -0
- package/skills/prompt-guard/ARCHITECTURE.md +364 -0
- package/skills/prompt-guard/CHANGELOG.md +200 -0
- package/skills/prompt-guard/README.md +215 -0
- package/skills/prompt-guard/SECURITY.md +66 -0
- package/skills/prompt-guard/SKILL.md +174 -0
- package/skills/prompt-guard/blog/how-i-secured-my-ai-agent.md +185 -0
- package/skills/prompt-guard/config.example.yaml +56 -0
- package/skills/prompt-guard/references/detection-patterns.md +298 -0
- package/skills/prompt-guard/requirements.txt +1 -0
- package/skills/prompt-guard/scripts/analyze_log.py +224 -0
- package/skills/prompt-guard/scripts/audit.py +344 -0
- package/skills/prompt-guard/scripts/detect.py +1587 -0
- package/skills/prompt-guard/scripts/hivefence.py +345 -0
- package/skills/proto-persona/SKILL.md +336 -0
- package/skills/proto-persona/examples/sample.md +97 -0
- package/skills/proto-persona/template.md +45 -0
- package/skills/recommendation-canvas/SKILL.md +382 -0
- package/skills/recommendation-canvas/examples/sample.md +94 -0
- package/skills/recommendation-canvas/template.md +86 -0
- package/skills/refactor-suggest/.clawhub/origin.json +7 -0
- package/skills/refactor-suggest/SKILL.md +94 -0
- package/skills/roadmap-planning/SKILL.md +506 -0
- package/skills/roadmap-planning/examples/sample.md +62 -0
- package/skills/roadmap-planning/template.md +30 -0
- package/skills/saas-economics-efficiency-metrics/SKILL.md +694 -0
- package/skills/saas-economics-efficiency-metrics/examples/cash-trap.md +365 -0
- package/skills/saas-economics-efficiency-metrics/examples/healthy-unit-economics.md +279 -0
- package/skills/saas-economics-efficiency-metrics/template.md +263 -0
- package/skills/saas-revenue-growth-metrics/SKILL.md +629 -0
- package/skills/saas-revenue-growth-metrics/examples/healthy-saas.md +131 -0
- package/skills/saas-revenue-growth-metrics/examples/warning-signs.md +229 -0
- package/skills/saas-revenue-growth-metrics/template.md +192 -0
- package/skills/save-money/.clawhub/origin.json +7 -0
- package/skills/save-money/SKILL.md +173 -0
- package/skills/scripts/golden_skills_v3.sh +32 -0
- package/skills/search/.clawhub/origin.json +7 -0
- package/skills/search/SKILL.md +18 -0
- package/skills/search/skill.json +1 -0
- package/skills/second-brain/.clawhub/origin.json +7 -0
- package/skills/second-brain/SKILL.md +278 -0
- package/skills/second-brain/scripts/ensue-api.sh +37 -0
- package/skills/self-improving-agent/.clawhub/origin.json +7 -0
- package/skills/self-improving-agent/.learnings/ERRORS.md +5 -0
- package/skills/self-improving-agent/.learnings/FEATURE_REQUESTS.md +5 -0
- package/skills/self-improving-agent/.learnings/LEARNINGS.md +5 -0
- package/skills/self-improving-agent/SKILL.md +130 -0
- package/skills/self-improving-agent/assets/LEARNINGS.md +45 -0
- package/skills/self-improving-agent/assets/SKILL-TEMPLATE.md +177 -0
- package/skills/self-improving-agent/hooks/openclaw/HOOK.md +23 -0
- package/skills/self-improving-agent/hooks/openclaw/handler.js +56 -0
- package/skills/self-improving-agent/hooks/openclaw/handler.ts +62 -0
- package/skills/self-improving-agent/references/examples.md +374 -0
- package/skills/self-improving-agent/references/hooks-setup.md +223 -0
- package/skills/self-improving-agent/references/openclaw-integration.md +248 -0
- package/skills/self-improving-agent/references/templates.md +480 -0
- package/skills/self-improving-agent/scripts/activator.sh +20 -0
- package/skills/self-improving-agent/scripts/error-detector.sh +55 -0
- package/skills/self-improving-agent/scripts/extract-skill.sh +203 -0
- package/skills/self-improving-agent-1-0-2/.clawhub/origin.json +7 -0
- package/skills/self-improving-agent-1-0-2/SKILL.md +562 -0
- package/skills/self-improving-agent-1-0-2/assets/LEARNINGS.md +45 -0
- package/skills/self-improving-agent-1-0-2/assets/SKILL-TEMPLATE.md +182 -0
- package/skills/self-improving-agent-1-0-2/references/clawdbot-integration.md +311 -0
- package/skills/self-improving-agent-1-0-2/references/examples.md +374 -0
- package/skills/self-improving-agent-1-0-2/references/hooks-setup.md +223 -0
- package/skills/self-improving-agent-1-0-2/scripts/activator.sh +20 -0
- package/skills/self-improving-agent-1-0-2/scripts/error-detector.sh +55 -0
- package/skills/self-improving-agent-1-0-2/scripts/extract-skill.sh +203 -0
- package/skills/self-love-confidence/.clawhub/origin.json +7 -0
- package/skills/self-love-confidence/SKILL.md +79 -0
- package/skills/self-reflection/.clawhub/origin.json +7 -0
- package/skills/self-reflection/README.md +292 -0
- package/skills/self-reflection/SKILL.md +110 -0
- package/skills/self-reflection/self-reflection.example.json +6 -0
- package/skills/slither-analyzer/SKILL.md +27 -0
- package/skills/solidity-audit/SKILL.md +27 -0
- package/skills/soulcraft/.clawhub/origin.json +7 -0
- package/skills/soulcraft/README.md +123 -0
- package/skills/soulcraft/SKILL.md +340 -0
- package/skills/soulcraft/references/question-bank.md +154 -0
- package/skills/soulcraft/references/soul-examples.md +207 -0
- package/skills/soulcraft/research/RESEARCH_REPORT.md +317 -0
- package/skills/spotify-player/.clawhub/origin.json +7 -0
- package/skills/spotify-player/SKILL.md +44 -0
- package/skills/storyboard/SKILL.md +259 -0
- package/skills/storyboard/examples/sample.md +71 -0
- package/skills/storyboard/template.md +41 -0
- package/skills/summarize/.clawhub/origin.json +7 -0
- package/skills/summarize/SKILL.md +59 -0
- package/skills/superdesign/.clawhub/origin.json +7 -0
- package/skills/superdesign/SKILL.md +224 -0
- package/skills/tam-sam-som-calculator/SKILL.md +399 -0
- package/skills/tam-sam-som-calculator/examples/sample.md +142 -0
- package/skills/tam-sam-som-calculator/scripts/market-sizing.py +95 -0
- package/skills/tam-sam-som-calculator/template.md +35 -0
- package/skills/tavily-search/.clawhub/origin.json +7 -0
- package/skills/tavily-search/SKILL.md +49 -0
- package/skills/tavily-search/scripts/extract.mjs +59 -0
- package/skills/tavily-search/scripts/search.mjs +101 -0
- package/skills/twitter/SKILL.md +74 -0
- package/skills/twitter/_meta.json +9 -0
- package/skills/twitter/references/validation-cases.md +53 -0
- package/skills/twitter/scripts/twitter.sh +421 -0
- package/skills/ui-ux-pro-max/.clawhub/origin.json +7 -0
- package/skills/ui-ux-pro-max/SKILL.md +54 -0
- package/skills/ui-ux-pro-max/assets/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/assets/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/assets/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/assets/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/assets/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/assets/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/assets/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/assets/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/assets/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/assets/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/assets/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/assets/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/references/upstream-README.md +488 -0
- package/skills/ui-ux-pro-max/references/upstream-skill-content.md +288 -0
- package/skills/ui-ux-pro-max/scripts/__init__.py +0 -0
- package/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1071 -0
- package/skills/ui-ux-pro-max/scripts/search.py +111 -0
- package/skills/user-story/SKILL.md +272 -0
- package/skills/user-story/examples/sample.md +110 -0
- package/skills/user-story/scripts/user-story-template.py +65 -0
- package/skills/user-story/template.md +32 -0
- package/skills/user-story-mapping/SKILL.md +296 -0
- package/skills/user-story-mapping/examples/sample.md +77 -0
- package/skills/user-story-mapping/template.md +41 -0
- package/skills/user-story-mapping-workshop/SKILL.md +485 -0
- package/skills/user-story-mapping-workshop/template.md +28 -0
- package/skills/user-story-splitting/SKILL.md +313 -0
- package/skills/user-story-splitting/examples/sample.md +147 -0
- package/skills/user-story-splitting/template.md +37 -0
- package/skills/wacli/.clawhub/origin.json +7 -0
- package/skills/wacli/SKILL.md +53 -0
- package/skills/web-search/.clawhub/origin.json +7 -0
- package/skills/web-search/SKILL.md +151 -0
- package/skills/web-search/references/api-details.md +207 -0
- package/skills/web-search/scripts/search.py +576 -0
- package/skills/workshop-facilitation/SKILL.md +88 -0
- package/skills/world-builder/SKILL.md +27 -0
- package/souls/blockchain-auditor/PRINCIPLES.md +75 -0
- package/souls/blockchain-auditor/SOUL.md +56 -0
- package/souls/blockchain-auditor/capabilities.json +33 -0
- package/souls/blockchain-auditor/evolution/capsules.json +4 -0
- package/souls/blockchain-auditor/evolution/events.jsonl +1 -0
- package/souls/blockchain-auditor/evolution/genes.json +62 -0
- package/souls/daedalus/PRINCIPLES.md +78 -0
- package/souls/daedalus/SOUL.md +48 -0
- package/souls/daedalus/capabilities.json +46 -0
- package/souls/identity-architect/PRINCIPLES.md +83 -0
- package/souls/identity-architect/SOUL.md +66 -0
- package/souls/identity-architect/capabilities.json +38 -0
- package/souls/identity-architect/evolution/capsules.json +4 -0
- package/souls/identity-architect/evolution/events.jsonl +0 -0
- package/souls/identity-architect/evolution/genes.json +4 -0
- package/souls/infra-ops/PRINCIPLES.md +77 -0
- package/souls/infra-ops/SOUL.md +56 -0
- package/souls/infra-ops/capabilities.json +33 -0
- package/souls/infra-ops/evolution/capsules.json +4 -0
- package/souls/infra-ops/evolution/events.jsonl +0 -0
- package/souls/infra-ops/evolution/genes.json +4 -0
- package/souls/lore-writer/PRINCIPLES.md +74 -0
- package/souls/lore-writer/SOUL.md +54 -0
- package/souls/lore-writer/capabilities.json +37 -0
- package/souls/lore-writer/evolution/capsules.json +4 -0
- package/souls/lore-writer/evolution/events.jsonl +0 -0
- package/souls/lore-writer/evolution/genes.json +4 -0
- package/souls/qa-evidence/PRINCIPLES.md +97 -0
- package/souls/qa-evidence/SOUL.md +66 -0
- package/souls/qa-evidence/capabilities.json +32 -0
- package/souls/qa-evidence/evolution/capsules.json +4 -0
- package/souls/qa-evidence/evolution/events.jsonl +0 -0
- package/souls/qa-evidence/evolution/genes.json +4 -0
- package/souls/registry.json +211 -0
- package/souls/sync-registry.js +65 -0
- package/uninstall.sh +102 -0
- package/workspace-bin/auto-checkpoint +60 -0
- package/workspace-bin/clawvault-access-control +65 -0
- package/workspace-bin/clawvault-local +28 -0
- package/workspace-bin/compile-boot +494 -0
- package/workspace-bin/daily-log-writer.mjs +251 -0
- package/workspace-bin/evolve +540 -0
- package/workspace-bin/fitness_score.py +395 -0
- package/workspace-bin/hooks/pre-commit +80 -0
- package/workspace-bin/install-daemon +299 -0
- package/workspace-bin/lane-watchdog.js +232 -0
- package/workspace-bin/lib/__init__.py +0 -0
- package/workspace-bin/lib/frontmatter.py +114 -0
- package/workspace-bin/memory-daemon.mjs +879 -0
- package/workspace-bin/memory-maintenance.mjs +531 -0
- package/workspace-bin/mesh-bridge.mjs +154 -0
- package/workspace-bin/multi-review +130 -0
- package/workspace-bin/obsidian +125 -0
- package/workspace-bin/obsidian-sync.mjs +888 -0
- package/workspace-bin/openclaw-register-source +102 -0
- package/workspace-bin/proactive-scan +147 -0
- package/workspace-bin/quality-gate +175 -0
- package/workspace-bin/screenshot +96 -0
- package/workspace-bin/session-recap +453 -0
- package/workspace-bin/skill-audit +494 -0
- package/workspace-bin/skill-quality-check +134 -0
- package/workspace-bin/skill-routing-eval +599 -0
- package/workspace-bin/soul-prompt +251 -0
- package/workspace-bin/subagent-audit.mjs +267 -0
- package/workspace-bin/test-multi-soul-workflow +130 -0
- package/workspace-bin/trust-registry +465 -0
- package/workspace-docs/AGENTS.md +201 -0
- package/workspace-docs/CLAUDE.md +64 -0
- package/workspace-docs/PRINCIPLES.md +81 -0
- 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
|
+
}
|