makersclaw 0.8.0 → 0.8.1
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/README.md +9 -3
- package/package.json +1 -1
- package/r/activity-log.json +52 -0
- package/r/agent-changes.json +51 -0
- package/r/agent-chat.json +53 -0
- package/r/agent-column.json +85 -0
- package/r/agent-dock.json +57 -0
- package/r/agent-kit.json +49 -0
- package/r/agent-marks.json +49 -0
- package/r/agent-pane.json +93 -0
- package/r/ai-action.json +56 -0
- package/r/annotate.json +3 -3
- package/r/approval-request.json +2 -2
- package/r/approval.json +54 -0
- package/r/ask-bar.json +50 -0
- package/r/audio-player.json +2 -2
- package/r/auth-form.json +2 -2
- package/r/automation-editor.json +66 -0
- package/r/automation-list.json +57 -0
- package/r/automation-model.json +118 -0
- package/r/automations-screen.json +71 -0
- package/r/board.json +2 -2
- package/r/calendar-view.json +3 -3
- package/r/cart.json +2 -2
- package/r/chart.json +2 -2
- package/r/chat-pane.json +84 -0
- package/r/chat.json +2 -2
- package/r/code-editor.json +2 -2
- package/r/comment-box.json +3 -3
- package/r/connect-card.json +51 -0
- package/r/contact-section.json +2 -2
- package/r/core.json +13 -7
- package/r/cost.json +51 -0
- package/r/csv-import.json +3 -3
- package/r/data-picker.json +60 -0
- package/r/data-table.json +2 -2
- package/r/diagram.json +2 -2
- package/r/doc-editor.json +2 -2
- package/r/doc-page.json +3 -3
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +2 -2
- package/r/first-run.json +63 -0
- package/r/fix-note.json +57 -0
- package/r/flow-canvas.json +2 -2
- package/r/flow-editor.json +79 -0
- package/r/footer.json +2 -2
- package/r/gantt.json +3 -3
- package/r/job-panel.json +2 -2
- package/r/json-editor.json +3 -3
- package/r/library-shelf.json +56 -0
- package/r/map.json +2 -2
- package/r/margin-notes.json +67 -0
- package/r/menubar.json +2 -2
- package/r/mind-map.json +3 -3
- package/r/navigation-menu.json +2 -2
- package/r/needs-you.json +55 -0
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +3 -3
- package/r/pace-bar.json +69 -0
- package/r/page-builder.json +2 -2
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +2 -2
- package/r/product-detail.json +2 -2
- package/r/product-grid.json +3 -3
- package/r/record-form.json +2 -2
- package/r/record-grid.json +52 -0
- package/r/record-notes.json +4 -4
- package/r/registry.json +4086 -1512
- package/r/run-history.json +52 -0
- package/r/run-view.json +80 -0
- package/r/schedule-card.json +54 -0
- package/r/schedule-engine.json +79 -0
- package/r/schedule-gantt.json +83 -0
- package/r/schedule-ruler.json +64 -0
- package/r/search-dialog.json +2 -2
- package/r/settings-list.json +2 -2
- package/r/setup-checklist.json +2 -2
- package/r/share-dialog.json +2 -2
- package/r/sidebar.json +2 -2
- package/r/signature-pad.json +2 -2
- package/r/sketch-canvas.json +2 -2
- package/r/source-receipt.json +52 -0
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +3 -3
- package/r/step-picker.json +53 -0
- package/r/step-settings.json +72 -0
- package/r/suggestion.json +58 -0
- package/r/sum-bar.json +64 -0
- package/r/task-list.json +3 -3
- package/r/task-model.json +2 -2
- package/r/testimonials.json +2 -2
- package/r/tier-map.json +67 -0
- package/r/time-grid.json +73 -0
- package/r/time-strip.json +64 -0
- package/r/top-bar.json +66 -0
- package/r/transaction-list.json +2 -2
- package/r/trigger-picker.json +61 -0
- package/r/whiteboard.json +3 -3
- package/r/workflow-view.json +55 -0
- package/registry/blocks/activity-log/README.md +112 -0
- package/registry/blocks/activity-log/activity-log.jsx +351 -0
- package/registry/blocks/activity-log/block.json +33 -0
- package/registry/blocks/activity-log/demo.jsx +48 -0
- package/registry/blocks/activity-log/index.js +4 -0
- package/registry/blocks/agent-changes/README.md +94 -0
- package/registry/blocks/agent-changes/agent-changes.jsx +118 -0
- package/registry/blocks/agent-changes/block.json +28 -0
- package/registry/blocks/agent-changes/demo.jsx +39 -0
- package/registry/blocks/agent-changes/index.js +3 -0
- package/registry/blocks/agent-chat/README.md +145 -0
- package/registry/blocks/agent-chat/agent-chat.jsx +415 -0
- package/registry/blocks/agent-chat/block.json +30 -0
- package/registry/blocks/agent-chat/demo.jsx +165 -0
- package/registry/blocks/agent-chat/index.js +3 -0
- package/registry/blocks/agent-column/README.md +197 -0
- package/registry/blocks/agent-column/agent-column.jsx +492 -0
- package/registry/blocks/agent-column/block.json +67 -0
- package/registry/blocks/agent-column/column-picker.jsx +163 -0
- package/registry/blocks/agent-column/demo.jsx +50 -0
- package/registry/blocks/agent-column/index.js +7 -0
- package/registry/blocks/agent-column/model.js +164 -0
- package/registry/blocks/agent-column/receipt.jsx +483 -0
- package/registry/blocks/agent-column/spreadsheet-type.jsx +24 -0
- package/registry/blocks/agent-dock/README.md +210 -0
- package/registry/blocks/agent-dock/agent-dock.jsx +356 -0
- package/registry/blocks/agent-dock/block.json +47 -0
- package/registry/blocks/agent-dock/demo.jsx +153 -0
- package/registry/blocks/agent-dock/dock-model.js +118 -0
- package/registry/blocks/agent-dock/index.js +5 -0
- package/registry/blocks/agent-kit/README.md +73 -0
- package/registry/blocks/agent-kit/agent-kit.jsx +86 -0
- package/registry/blocks/agent-kit/block.json +10 -0
- package/registry/blocks/agent-kit/demo.jsx +35 -0
- package/registry/blocks/agent-kit/index.js +3 -0
- package/registry/blocks/agent-marks/README.md +80 -0
- package/registry/blocks/agent-marks/agent-marks.jsx +133 -0
- package/registry/blocks/agent-marks/block.json +27 -0
- package/registry/blocks/agent-marks/demo.jsx +49 -0
- package/registry/blocks/agent-marks/index.js +3 -0
- package/registry/blocks/agent-pane/README.md +225 -0
- package/registry/blocks/agent-pane/agent-pane.jsx +244 -0
- package/registry/blocks/agent-pane/block.json +41 -0
- package/registry/blocks/agent-pane/composer.jsx +6 -0
- package/registry/blocks/agent-pane/demo.jsx +233 -0
- package/registry/blocks/agent-pane/dock-context.js +10 -0
- package/registry/blocks/agent-pane/index.js +4 -0
- package/registry/blocks/agent-pane/prose.jsx +84 -0
- package/registry/blocks/agent-pane/questions.jsx +134 -0
- package/registry/blocks/agent-pane/transcript.jsx +268 -0
- package/registry/blocks/agent-pane/work.jsx +368 -0
- package/registry/blocks/ai-action/README.md +94 -0
- package/registry/blocks/ai-action/ai-action.jsx +188 -0
- package/registry/blocks/ai-action/block.json +34 -0
- package/registry/blocks/ai-action/demo.jsx +93 -0
- package/registry/blocks/ai-action/index.js +3 -0
- package/registry/blocks/approval/README.md +145 -0
- package/registry/blocks/approval/approval.jsx +163 -0
- package/registry/blocks/approval/block.json +31 -0
- package/registry/blocks/approval/demo.jsx +121 -0
- package/registry/blocks/approval/index.js +3 -0
- package/registry/blocks/ask-bar/README.md +105 -0
- package/registry/blocks/ask-bar/ask-bar.jsx +166 -0
- package/registry/blocks/ask-bar/block.json +26 -0
- package/registry/blocks/ask-bar/demo.jsx +78 -0
- package/registry/blocks/ask-bar/index.js +3 -0
- package/registry/blocks/automation-editor/README.md +291 -0
- package/registry/blocks/automation-editor/automation-editor.jsx +656 -0
- package/registry/blocks/automation-editor/block.json +39 -0
- package/registry/blocks/automation-editor/demo.jsx +284 -0
- package/registry/blocks/automation-editor/index.js +4 -0
- package/registry/blocks/automation-editor/run-inputs.jsx +82 -0
- package/registry/blocks/automation-list/README.md +129 -0
- package/registry/blocks/automation-list/automation-list.jsx +476 -0
- package/registry/blocks/automation-list/block.json +35 -0
- package/registry/blocks/automation-list/demo.jsx +67 -0
- package/registry/blocks/automation-list/index.js +3 -0
- package/registry/blocks/automation-model/README.md +109 -0
- package/registry/blocks/automation-model/block.json +135 -0
- package/registry/blocks/automation-model/catalog.js +256 -0
- package/registry/blocks/automation-model/check.js +161 -0
- package/registry/blocks/automation-model/demo.jsx +42 -0
- package/registry/blocks/automation-model/edit.js +262 -0
- package/registry/blocks/automation-model/fixes.js +287 -0
- package/registry/blocks/automation-model/glyph.jsx +13 -0
- package/registry/blocks/automation-model/graph.js +410 -0
- package/registry/blocks/automation-model/index.js +33 -0
- package/registry/blocks/automation-model/overlay.js +175 -0
- package/registry/blocks/automation-model/parts.jsx +94 -0
- package/registry/blocks/automation-model/schedule.js +53 -0
- package/registry/blocks/automation-model/templates.js +128 -0
- package/registry/blocks/automation-model/values.js +29 -0
- package/registry/blocks/automation-model/words.js +439 -0
- package/registry/blocks/automations-screen/README.md +207 -0
- package/registry/blocks/automations-screen/automations-screen.jsx +452 -0
- package/registry/blocks/automations-screen/block.json +47 -0
- package/registry/blocks/automations-screen/demo.jsx +364 -0
- package/registry/blocks/automations-screen/index.js +4 -0
- package/registry/blocks/automations-screen/screen-model.js +129 -0
- package/registry/blocks/chat-pane/README.md +143 -0
- package/registry/blocks/chat-pane/block.json +48 -0
- package/registry/blocks/chat-pane/chat-attach.js +166 -0
- package/registry/blocks/chat-pane/chat-composer.jsx +195 -0
- package/registry/blocks/chat-pane/chat-context.js +17 -0
- package/registry/blocks/chat-pane/chat-model.js +43 -0
- package/registry/blocks/chat-pane/chat-pane.jsx +398 -0
- package/registry/blocks/chat-pane/chats-list.jsx +52 -0
- package/registry/blocks/chat-pane/demo.jsx +103 -0
- package/registry/blocks/chat-pane/index.js +9 -0
- package/registry/blocks/connect-card/README.md +99 -0
- package/registry/blocks/connect-card/block.json +10 -0
- package/registry/blocks/connect-card/connect-card.jsx +103 -0
- package/registry/blocks/connect-card/demo.jsx +49 -0
- package/registry/blocks/connect-card/index.js +3 -0
- package/registry/blocks/cost/README.md +115 -0
- package/registry/blocks/cost/block.json +10 -0
- package/registry/blocks/cost/cost.jsx +176 -0
- package/registry/blocks/cost/demo.jsx +66 -0
- package/registry/blocks/cost/index.js +3 -0
- package/registry/blocks/data-picker/README.md +81 -0
- package/registry/blocks/data-picker/block.json +38 -0
- package/registry/blocks/data-picker/data-picker.jsx +210 -0
- package/registry/blocks/data-picker/demo.jsx +34 -0
- package/registry/blocks/data-picker/fields.js +89 -0
- package/registry/blocks/data-picker/index.js +4 -0
- package/registry/blocks/data-table/demo.jsx +1 -1
- package/registry/blocks/first-run/README.md +118 -0
- package/registry/blocks/first-run/block.json +11 -0
- package/registry/blocks/first-run/demo.jsx +88 -0
- package/registry/blocks/first-run/first-run-model.js +51 -0
- package/registry/blocks/first-run/first-run.jsx +156 -0
- package/registry/blocks/first-run/index.js +5 -0
- package/registry/blocks/first-run/probe-rules.js +26 -0
- package/registry/blocks/fix-note/README.md +79 -0
- package/registry/blocks/fix-note/block.json +35 -0
- package/registry/blocks/fix-note/demo.jsx +30 -0
- package/registry/blocks/fix-note/fix-note.jsx +235 -0
- package/registry/blocks/fix-note/index.js +3 -0
- package/registry/blocks/flow-editor/README.md +108 -0
- package/registry/blocks/flow-editor/block.json +44 -0
- package/registry/blocks/flow-editor/demo.jsx +115 -0
- package/registry/blocks/flow-editor/flow-canvas.jsx +568 -0
- package/registry/blocks/flow-editor/flow-editor.jsx +154 -0
- package/registry/blocks/flow-editor/index.js +6 -0
- package/registry/blocks/flow-editor/minimap.jsx +78 -0
- package/registry/blocks/flow-editor/outline.jsx +134 -0
- package/registry/blocks/library-shelf/README.md +117 -0
- package/registry/blocks/library-shelf/block.json +28 -0
- package/registry/blocks/library-shelf/demo.jsx +68 -0
- package/registry/blocks/library-shelf/index.js +3 -0
- package/registry/blocks/library-shelf/library-shelf.jsx +211 -0
- package/registry/blocks/library-shelf/probe-rules.js +15 -0
- package/registry/blocks/margin-notes/README.md +222 -0
- package/registry/blocks/margin-notes/block.json +10 -0
- package/registry/blocks/margin-notes/demo.jsx +144 -0
- package/registry/blocks/margin-notes/index.js +20 -0
- package/registry/blocks/margin-notes/margin-model.js +224 -0
- package/registry/blocks/margin-notes/margin-notes.jsx +465 -0
- package/registry/blocks/needs-you/README.md +177 -0
- package/registry/blocks/needs-you/block.json +34 -0
- package/registry/blocks/needs-you/demo.jsx +143 -0
- package/registry/blocks/needs-you/index.js +3 -0
- package/registry/blocks/needs-you/needs-you.jsx +441 -0
- package/registry/blocks/pace-bar/README.md +230 -0
- package/registry/blocks/pace-bar/block.json +10 -0
- package/registry/blocks/pace-bar/demo.jsx +255 -0
- package/registry/blocks/pace-bar/index.js +22 -0
- package/registry/blocks/pace-bar/pace-bar.jsx +461 -0
- package/registry/blocks/pace-bar/pace-model.js +288 -0
- package/registry/blocks/record-grid/README.md +100 -0
- package/registry/blocks/record-grid/block.json +28 -0
- package/registry/blocks/record-grid/demo.jsx +43 -0
- package/registry/blocks/record-grid/index.js +3 -0
- package/registry/blocks/record-grid/record-grid.jsx +251 -0
- package/registry/blocks/run-history/README.md +138 -0
- package/registry/blocks/run-history/block.json +10 -0
- package/registry/blocks/run-history/demo.jsx +74 -0
- package/registry/blocks/run-history/index.js +3 -0
- package/registry/blocks/run-history/run-history.jsx +218 -0
- package/registry/blocks/run-view/README.md +174 -0
- package/registry/blocks/run-view/block.json +50 -0
- package/registry/blocks/run-view/demo.jsx +286 -0
- package/registry/blocks/run-view/index.js +5 -0
- package/registry/blocks/run-view/run-controls.jsx +266 -0
- package/registry/blocks/run-view/run-view.jsx +236 -0
- package/registry/blocks/run-view/runs-list.jsx +99 -0
- package/registry/blocks/schedule-card/README.md +121 -0
- package/registry/blocks/schedule-card/block.json +10 -0
- package/registry/blocks/schedule-card/demo.jsx +82 -0
- package/registry/blocks/schedule-card/index.js +3 -0
- package/registry/blocks/schedule-card/schedule-card.jsx +136 -0
- package/registry/blocks/schedule-engine/README.md +137 -0
- package/registry/blocks/schedule-engine/block.json +10 -0
- package/registry/blocks/schedule-engine/demo.jsx +82 -0
- package/registry/blocks/schedule-engine/engine.js +769 -0
- package/registry/blocks/schedule-engine/index.js +9 -0
- package/registry/blocks/schedule-engine/ops.js +71 -0
- package/registry/blocks/schedule-engine/place.js +294 -0
- package/registry/blocks/schedule-engine/tree.js +79 -0
- package/registry/blocks/schedule-engine/waits.js +80 -0
- package/registry/blocks/schedule-gantt/README.md +159 -0
- package/registry/blocks/schedule-gantt/block.json +10 -0
- package/registry/blocks/schedule-gantt/demo.jsx +223 -0
- package/registry/blocks/schedule-gantt/gantt-model.js +219 -0
- package/registry/blocks/schedule-gantt/geometry.js +176 -0
- package/registry/blocks/schedule-gantt/index.js +26 -0
- package/registry/blocks/schedule-gantt/schedule-gantt.jsx +882 -0
- package/registry/blocks/schedule-gantt/sheet.jsx +372 -0
- package/registry/blocks/schedule-gantt/track.jsx +190 -0
- package/registry/blocks/schedule-ruler/README.md +148 -0
- package/registry/blocks/schedule-ruler/block.json +10 -0
- package/registry/blocks/schedule-ruler/demo.jsx +181 -0
- package/registry/blocks/schedule-ruler/index.js +21 -0
- package/registry/blocks/schedule-ruler/ruler-model.js +239 -0
- package/registry/blocks/schedule-ruler/schedule-ruler.jsx +548 -0
- package/registry/blocks/source-receipt/README.md +97 -0
- package/registry/blocks/source-receipt/block.json +30 -0
- package/registry/blocks/source-receipt/demo.jsx +71 -0
- package/registry/blocks/source-receipt/index.js +3 -0
- package/registry/blocks/source-receipt/source-receipt.jsx +107 -0
- package/registry/blocks/spreadsheet/spreadsheet.jsx +15 -1
- package/registry/blocks/step-picker/README.md +78 -0
- package/registry/blocks/step-picker/block.json +31 -0
- package/registry/blocks/step-picker/demo.jsx +23 -0
- package/registry/blocks/step-picker/index.js +3 -0
- package/registry/blocks/step-picker/step-picker.jsx +144 -0
- package/registry/blocks/step-settings/README.md +100 -0
- package/registry/blocks/step-settings/block.json +44 -0
- package/registry/blocks/step-settings/demo.jsx +93 -0
- package/registry/blocks/step-settings/fields.jsx +58 -0
- package/registry/blocks/step-settings/flow-settings.jsx +115 -0
- package/registry/blocks/step-settings/index.js +5 -0
- package/registry/blocks/step-settings/step-settings.jsx +290 -0
- package/registry/blocks/suggestion/README.md +171 -0
- package/registry/blocks/suggestion/block.json +40 -0
- package/registry/blocks/suggestion/demo.jsx +91 -0
- package/registry/blocks/suggestion/index.js +3 -0
- package/registry/blocks/suggestion/suggestion.jsx +436 -0
- package/registry/blocks/sum-bar/README.md +122 -0
- package/registry/blocks/sum-bar/block.json +10 -0
- package/registry/blocks/sum-bar/demo.jsx +110 -0
- package/registry/blocks/sum-bar/index.js +5 -0
- package/registry/blocks/sum-bar/sum-bar.jsx +209 -0
- package/registry/blocks/sum-bar/sum-model.js +126 -0
- package/registry/blocks/tier-map/README.md +217 -0
- package/registry/blocks/tier-map/block.json +10 -0
- package/registry/blocks/tier-map/demo.jsx +222 -0
- package/registry/blocks/tier-map/index.js +24 -0
- package/registry/blocks/tier-map/tier-map.jsx +574 -0
- package/registry/blocks/tier-map/tier-model.js +318 -0
- package/registry/blocks/time-grid/README.md +305 -0
- package/registry/blocks/time-grid/block.json +10 -0
- package/registry/blocks/time-grid/demo.jsx +279 -0
- package/registry/blocks/time-grid/grid-model.js +322 -0
- package/registry/blocks/time-grid/index.js +25 -0
- package/registry/blocks/time-grid/time-grid.jsx +1325 -0
- package/registry/blocks/time-strip/README.md +141 -0
- package/registry/blocks/time-strip/block.json +10 -0
- package/registry/blocks/time-strip/demo.jsx +129 -0
- package/registry/blocks/time-strip/index.js +19 -0
- package/registry/blocks/time-strip/strip-model.js +178 -0
- package/registry/blocks/time-strip/time-strip.jsx +260 -0
- package/registry/blocks/top-bar/README.md +149 -0
- package/registry/blocks/top-bar/block.json +35 -0
- package/registry/blocks/top-bar/demo.jsx +78 -0
- package/registry/blocks/top-bar/index.js +5 -0
- package/registry/blocks/top-bar/probe-rules.js +74 -0
- package/registry/blocks/top-bar/top-bar-model.js +152 -0
- package/registry/blocks/top-bar/top-bar.jsx +244 -0
- package/registry/blocks/trigger-picker/README.md +98 -0
- package/registry/blocks/trigger-picker/block.json +38 -0
- package/registry/blocks/trigger-picker/demo.jsx +59 -0
- package/registry/blocks/trigger-picker/index.js +3 -0
- package/registry/blocks/trigger-picker/trigger-picker.jsx +374 -0
- package/registry/blocks/workflow-view/README.md +123 -0
- package/registry/blocks/workflow-view/block.json +10 -0
- package/registry/blocks/workflow-view/demo.jsx +90 -0
- package/registry/blocks/workflow-view/index.js +3 -0
- package/registry/blocks/workflow-view/workflow-view.jsx +203 -0
- package/src/install.mjs +77 -16
- package/src/registry.mjs +3 -0
- package/templates/next/browser/lib/data.js +6 -1
- package/templates/next/files/README.md +627 -24
- package/templates/next/files/check/automation-format.ts +2103 -0
- package/templates/next/files/check/automation-package.ts +142 -0
- package/templates/next/files/check/run.mjs +968 -9
- package/templates/next/files/components/ui/field.jsx +17 -4
- package/templates/next/files/components/ui/icon.jsx +13 -0
- package/templates/next/files/components/ui/index.js +2 -1
- package/templates/next/files/components/ui/layout.jsx +77 -2
- package/templates/next/files/components/ui/notice.jsx +70 -0
- package/templates/next/files/components/ui/page.jsx +124 -0
- package/templates/next/files/components/ui/stat.jsx +22 -7
- package/templates/next/files/components/ui/toast.jsx +6 -3
- package/templates/next/files/lib/automations.js +660 -0
- package/templates/next/files/lib/claw.js +241 -5
- package/templates/next/files/lib/door.js +92 -0
- package/templates/next/files/lib/hooks.js +1627 -8
- package/templates/next/files/lib/platform.js +529 -21
- package/templates/next/template.json +13 -3
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Icon, cx } from "../../ui";
|
|
5
|
+
import { datesOf, dayOf, isoOf, linkProblem, makeCalendar, schedule, shiftDependents, spanOf, taskTree, waitsWords } from "../schedule-engine";
|
|
6
|
+
import { ZOOMS, axisFor, rangeFor, routeLink } from "./geometry";
|
|
7
|
+
import { baselineDelta, barLabel, columnsFor, dayWords, describeDrag, dragLines, fixedWidth, gapOf, linkLines, selectionMoves, slackWords } from "./gantt-model";
|
|
8
|
+
import { AddRow, ROW, SheetHead, SheetRow } from "./sheet";
|
|
9
|
+
import { HEAD, Ghosts, LaneLabels, LaneTrack, Links, Paper, TrackHead } from "./track";
|
|
10
|
+
|
|
11
|
+
// ScheduleGantt: the task sheet beside a working-day time scale, one picture
|
|
12
|
+
// row for row. Bars are steel grey with their progress inside; the critical
|
|
13
|
+
// path turns red when the page switches it on; a phase is a bracket over its
|
|
14
|
+
// work; a milestone a diamond. The target is a dashed line, the finish a solid
|
|
15
|
+
// one, and the gap between them is measured in working days in the header.
|
|
16
|
+
// Dashed outlines show where bars were (`ghosts`) and where a suggestion
|
|
17
|
+
// would put them (`pencil`).
|
|
18
|
+
//
|
|
19
|
+
// Drag a bar to move it, an end to change its length, the dot at its end onto
|
|
20
|
+
// another bar to link them: everything that waits on it moves while you drag,
|
|
21
|
+
// and a note by the bar says what it costs. README.md lists every prop.
|
|
22
|
+
// Built from divs and one svg for the links, tokens only.
|
|
23
|
+
|
|
24
|
+
const BAR = 14;
|
|
25
|
+
const CAL = makeCalendar("weekdays");
|
|
26
|
+
const EMPTY = [];
|
|
27
|
+
const NONE = new Set();
|
|
28
|
+
const KIND_OF = { "finish:start": "finish_to_start", "start:start": "start_to_start", "finish:finish": "finish_to_finish", "start:finish": "start_to_finish" };
|
|
29
|
+
const NUM = "tabular-nums";
|
|
30
|
+
|
|
31
|
+
const firstName = (name) => String(name ?? "").trim().split(/\s+/)[0] ?? "";
|
|
32
|
+
|
|
33
|
+
export function ScheduleGantt({
|
|
34
|
+
tasks: tasksProp,
|
|
35
|
+
links: linksProp,
|
|
36
|
+
people,
|
|
37
|
+
today,
|
|
38
|
+
showToday = true,
|
|
39
|
+
target,
|
|
40
|
+
zoom = "week",
|
|
41
|
+
fitWidth,
|
|
42
|
+
columns,
|
|
43
|
+
vsLabel = "vs baseline",
|
|
44
|
+
rowH = ROW,
|
|
45
|
+
gridWidth = 470,
|
|
46
|
+
onGridWidth,
|
|
47
|
+
height = "fill",
|
|
48
|
+
critical = false,
|
|
49
|
+
selectedId,
|
|
50
|
+
selectedIds,
|
|
51
|
+
onSelect,
|
|
52
|
+
collapsed,
|
|
53
|
+
onToggle,
|
|
54
|
+
onOpen,
|
|
55
|
+
onMove,
|
|
56
|
+
onLink,
|
|
57
|
+
onEdit,
|
|
58
|
+
onAdd,
|
|
59
|
+
onPreview,
|
|
60
|
+
onOpenLink,
|
|
61
|
+
highlight,
|
|
62
|
+
addUnder,
|
|
63
|
+
editRequest,
|
|
64
|
+
jump,
|
|
65
|
+
ghosts,
|
|
66
|
+
pencil,
|
|
67
|
+
baseline,
|
|
68
|
+
guesses,
|
|
69
|
+
notes,
|
|
70
|
+
tags,
|
|
71
|
+
moving,
|
|
72
|
+
rowMenu,
|
|
73
|
+
lanes,
|
|
74
|
+
lanesTitle,
|
|
75
|
+
loading = false,
|
|
76
|
+
error = null,
|
|
77
|
+
emptyText = "No tasks yet. Type the first one in the last row.",
|
|
78
|
+
label = "Gantt chart",
|
|
79
|
+
className,
|
|
80
|
+
}) {
|
|
81
|
+
const tasks = tasksProp ?? EMPTY;
|
|
82
|
+
const links = linksProp ?? EMPTY;
|
|
83
|
+
const scroller = useRef(null);
|
|
84
|
+
const track = useRef(null);
|
|
85
|
+
const dragRef = useRef(null);
|
|
86
|
+
const linkRef = useRef(null);
|
|
87
|
+
const suppress = useRef(false);
|
|
88
|
+
const addRef = useRef(null);
|
|
89
|
+
const [boxW, setBoxW] = useState(null);
|
|
90
|
+
const [preview, setPreview] = useState(null);
|
|
91
|
+
const [linking, setLinking] = useState(null);
|
|
92
|
+
const [editing, setEditing] = useState(null);
|
|
93
|
+
const [draft, setDraft] = useState({ title: "", days: "", milestone: false, parentId: null, afterId: null });
|
|
94
|
+
const [adding, setAdding] = useState(false);
|
|
95
|
+
const [hovered, setHovered] = useState(null);
|
|
96
|
+
const [busy, setBusy] = useState(false);
|
|
97
|
+
const [note, setNote] = useState(null);
|
|
98
|
+
const [gridState, setGridState] = useState(gridWidth);
|
|
99
|
+
const selected = selectedId ? String(selectedId) : null;
|
|
100
|
+
const picked = selectedIds ?? NONE;
|
|
101
|
+
|
|
102
|
+
// Measured in the callback, applied in the next frame (an observer that sets what it watches loops).
|
|
103
|
+
useLayoutEffect(() => {
|
|
104
|
+
const el = scroller.current;
|
|
105
|
+
if (!el) return undefined;
|
|
106
|
+
let raf = 0;
|
|
107
|
+
const apply = (w) => {
|
|
108
|
+
cancelAnimationFrame(raf);
|
|
109
|
+
raf = requestAnimationFrame(() => setBoxW(w));
|
|
110
|
+
};
|
|
111
|
+
setBoxW(el.clientWidth);
|
|
112
|
+
const onResize = () => apply(el.clientWidth);
|
|
113
|
+
window.addEventListener("resize", onResize);
|
|
114
|
+
const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => apply(el.clientWidth));
|
|
115
|
+
ro?.observe(el);
|
|
116
|
+
return () => {
|
|
117
|
+
cancelAnimationFrame(raf);
|
|
118
|
+
window.removeEventListener("resize", onResize);
|
|
119
|
+
ro?.disconnect();
|
|
120
|
+
};
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
// The page opens the add row: { id (a parent) | null, afterId, milestone, nonce }.
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!addUnder) return;
|
|
126
|
+
setAdding(true);
|
|
127
|
+
setDraft({ title: "", days: "", milestone: Boolean(addUnder.milestone), parentId: addUnder.id ? String(addUnder.id) : null, afterId: addUnder.afterId ? String(addUnder.afterId) : null });
|
|
128
|
+
const t = setTimeout(() => {
|
|
129
|
+
addRef.current?.focus();
|
|
130
|
+
addRef.current?.scrollIntoView?.({ block: "nearest" });
|
|
131
|
+
}, 20);
|
|
132
|
+
return () => clearTimeout(t);
|
|
133
|
+
}, [addUnder]);
|
|
134
|
+
|
|
135
|
+
// The page opens a cell: { id, field, nonce } (the row menu's Rename).
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (editRequest?.id) setEditing({ id: String(editRequest.id), field: editRequest.field ?? "title" });
|
|
138
|
+
}, [editRequest]);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (jump?.date) scrollToDay(jump.date, true);
|
|
142
|
+
}, [jump]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
143
|
+
|
|
144
|
+
// ---- the model ----------------------------------------------------------
|
|
145
|
+
const plan = useMemo(() => schedule(tasks, links), [tasks, links]);
|
|
146
|
+
const live = preview?.plan ?? plan;
|
|
147
|
+
const tree = useMemo(() => taskTree(tasks), [tasks]);
|
|
148
|
+
const byId = useMemo(() => new Map(tasks.map((t) => [String(t.id), t])), [tasks]);
|
|
149
|
+
const closed = collapsed ?? NONE;
|
|
150
|
+
const visible = useMemo(() => {
|
|
151
|
+
const out = [];
|
|
152
|
+
for (const e of tree.order) {
|
|
153
|
+
let hidden = false;
|
|
154
|
+
for (let p = e.parentId; p; p = tree.byId.get(p)?.parentId) if (closed.has(p)) hidden = true;
|
|
155
|
+
if (!hidden) out.push(e);
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}, [tree, closed]);
|
|
159
|
+
const rowOf = useMemo(() => new Map(visible.map((e, i) => [e.id, i])), [visible]);
|
|
160
|
+
const outlineOf = (id) => tree.byId.get(String(id))?.outline;
|
|
161
|
+
// The add row sits under the row it adds after; everything below it moves down by its height.
|
|
162
|
+
const ADD_H = ROW + 6;
|
|
163
|
+
const addIndex = adding ? (draft.afterId && rowOf.has(draft.afterId) ? rowOf.get(draft.afterId) : draft.parentId && rowOf.has(draft.parentId) ? rowOf.get(draft.parentId) : visible.length - 1) : null;
|
|
164
|
+
const rowTop = (i) => i * rowH + (addIndex != null && i > addIndex ? ADD_H : 0);
|
|
165
|
+
const rowAt = (y) => {
|
|
166
|
+
for (let i = 0; i < visible.length; i += 1) if (y >= rowTop(i) && y < rowTop(i) + rowH) return i;
|
|
167
|
+
return -1;
|
|
168
|
+
};
|
|
169
|
+
const shownRow = (id) => {
|
|
170
|
+
for (let cur = String(id); cur; cur = tree.byId.get(cur)?.parentId) if (rowOf.has(cur)) return rowOf.get(cur);
|
|
171
|
+
return null;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const shown = useMemo(() => {
|
|
175
|
+
const out = new Map();
|
|
176
|
+
for (const t of tasks) {
|
|
177
|
+
const id = String(t.id);
|
|
178
|
+
const r = live.rows.get(id);
|
|
179
|
+
if (!r || !r.dated || r.summary) continue;
|
|
180
|
+
const p = preview?.dates?.get(id);
|
|
181
|
+
out.set(id, { start: p?.start ?? r.start, due: p?.due ?? r.due });
|
|
182
|
+
}
|
|
183
|
+
for (const r of live.rows.values()) {
|
|
184
|
+
if (!r.summary) continue;
|
|
185
|
+
let s = null;
|
|
186
|
+
let d = null;
|
|
187
|
+
for (const leaf of r.leafIds) {
|
|
188
|
+
const x = out.get(leaf);
|
|
189
|
+
if (!x || !x.start) continue;
|
|
190
|
+
if (!s || x.start < s) s = x.start;
|
|
191
|
+
if (!d || x.due > d) d = x.due;
|
|
192
|
+
}
|
|
193
|
+
if (s) out.set(r.id, { start: s, due: d });
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}, [tasks, live, preview]);
|
|
197
|
+
|
|
198
|
+
// ---- the sheet ----------------------------------------------------------
|
|
199
|
+
const narrow = boxW != null && boxW < 760;
|
|
200
|
+
const cols = useMemo(() => columnsFor(columns, { vsLabel, narrow, outlineWidth: tree.order.some((e) => e.depth >= 3) ? 78 : undefined }), [columns, vsLabel, narrow, tree]);
|
|
201
|
+
const gridW = narrow ? Math.max(170, Math.min(260, Math.round((boxW ?? 400) * 0.46))) : Math.max(fixedWidth(cols) + 160, gridState);
|
|
202
|
+
const cellsOf = useMemo(() => {
|
|
203
|
+
const byTask = new Map();
|
|
204
|
+
for (const l of links) {
|
|
205
|
+
const k = String(l.task_id);
|
|
206
|
+
if (!byTask.has(k)) byTask.set(k, []);
|
|
207
|
+
byTask.get(k).push(l);
|
|
208
|
+
}
|
|
209
|
+
return (id) => {
|
|
210
|
+
const t = byId.get(id);
|
|
211
|
+
const r = live.rows.get(id);
|
|
212
|
+
const mine = byTask.get(id) ?? [];
|
|
213
|
+
const person = people?.get?.(String(t?.assignee_id ?? ""));
|
|
214
|
+
const name = person?.name ?? t?.assignee_name ?? "";
|
|
215
|
+
const out = {
|
|
216
|
+
owner: { name: firstName(name), title: name, left: Boolean(person?.left) },
|
|
217
|
+
waits: waitsWords(mine, id, outlineOf),
|
|
218
|
+
waitsLinks: mine.map((l) => live.links.get(String(l.id))).filter(Boolean),
|
|
219
|
+
waitsBroken: mine.some((l) => live.links.get(String(l.id))?.violated) || Boolean(r?.stuck),
|
|
220
|
+
slack: slackWords(r),
|
|
221
|
+
guessed: Boolean(guesses?.has?.(id)),
|
|
222
|
+
};
|
|
223
|
+
if (baseline) out.delta = baselineDelta(r, baseline.get?.(id));
|
|
224
|
+
return out;
|
|
225
|
+
};
|
|
226
|
+
}, [links, byId, live, people, baseline, guesses, tree]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
227
|
+
|
|
228
|
+
// ---- the axis -----------------------------------------------------------
|
|
229
|
+
const z = ZOOMS[zoom] ? zoom : "week";
|
|
230
|
+
const range = useMemo(() => {
|
|
231
|
+
const dates = [];
|
|
232
|
+
for (const r of plan.rows.values()) if (r.dated) dates.push(r.start, r.due, r.planDue);
|
|
233
|
+
if (target) dates.push(target);
|
|
234
|
+
if (ghosts) for (const g of ghosts.values()) dates.push(g.start, g.due);
|
|
235
|
+
if (pencil) for (const g of pencil.values()) dates.push(g.start, g.due);
|
|
236
|
+
if (baseline) for (const g of baseline.values()) dates.push(g.start, g.due);
|
|
237
|
+
return rangeFor(z, dates, today);
|
|
238
|
+
}, [plan, target, ghosts, pencil, baseline, z, today]);
|
|
239
|
+
// Whole project: the span fits the width, with room on the right for the notes after the bars.
|
|
240
|
+
const ppd = z === "fit" ? Math.max(2, ((fitWidth ?? boxW ?? 1200) - gridW - (notes?.size ? 200 : 16)) / (range.to - range.from + 1)) : ZOOMS[z].ppd;
|
|
241
|
+
const axis = useMemo(() => axisFor(z, range.from, range.to), [z, range.from, range.to]);
|
|
242
|
+
const trackW = Math.round((range.to - range.from + 1) * ppd);
|
|
243
|
+
const X = (iso) => (dayOf(iso) - range.from) * ppd;
|
|
244
|
+
const inRange = (iso) => iso && dayOf(iso) >= range.from && dayOf(iso) <= range.to;
|
|
245
|
+
|
|
246
|
+
const boxOf = (id) => {
|
|
247
|
+
const row = shownRow(id);
|
|
248
|
+
const d = shown.get(String(id));
|
|
249
|
+
if (row == null || !d) return null;
|
|
250
|
+
const r = live.rows.get(String(id));
|
|
251
|
+
const y = rowTop(row) + rowH / 2;
|
|
252
|
+
const own = rowOf.get(String(id)) === row;
|
|
253
|
+
if (r?.milestone) {
|
|
254
|
+
const cx0 = X(d.due) + ppd / 2;
|
|
255
|
+
return { x0: cx0 - 7, x1: cx0 + 7, y, top: y - 7, bottom: y + 7, milestone: true, row, own };
|
|
256
|
+
}
|
|
257
|
+
const h = r?.summary || !own ? 6 : BAR;
|
|
258
|
+
return { x0: X(d.start), x1: X(d.due) + ppd, y, top: y - h / 2, bottom: y + h / 2, milestone: false, row, own };
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const lines = useMemo(() => {
|
|
262
|
+
const out = [];
|
|
263
|
+
const moved = new Set((preview?.changes ?? []).map((c) => String(c.id)));
|
|
264
|
+
for (const l of live.links.values()) {
|
|
265
|
+
if (!l.valid) continue;
|
|
266
|
+
const a = boxOf(l.dependsOnId);
|
|
267
|
+
const b = boxOf(l.taskId);
|
|
268
|
+
if (!a || !b || a.row === b.row || !a.own || !b.own) continue;
|
|
269
|
+
const kind = { finish_to_start: "FS", start_to_start: "SS", finish_to_finish: "FF", start_to_finish: "SF" }[l.kind] ?? l.kind;
|
|
270
|
+
const route = routeLink(a, b, kind, { rowH });
|
|
271
|
+
out.push({ id: l.id, link: l, critical: l.critical, lag: l.lag, route, touches: selected === l.taskId || selected === l.dependsOnId, moving: moved.has(l.taskId) && moved.has(l.dependsOnId) });
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}, [live, shown, visible, ppd, range.from, selected, rowH, preview]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
275
|
+
|
|
276
|
+
function scrollToDay(iso, smooth) {
|
|
277
|
+
const el = scroller.current;
|
|
278
|
+
if (!el || !iso) return;
|
|
279
|
+
const left = Math.max(0, X(iso) - (el.clientWidth - gridW) * 0.3);
|
|
280
|
+
el.scrollTo({ left, behavior: smooth ? "smooth" : "auto" });
|
|
281
|
+
}
|
|
282
|
+
useLayoutEffect(() => {
|
|
283
|
+
if (z !== "fit") scrollToDay(today, false);
|
|
284
|
+
}, [z, range.from]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
285
|
+
|
|
286
|
+
// ---- dragging bars ----------------------------------------------------------
|
|
287
|
+
function movesFor(d, delta) {
|
|
288
|
+
const t = byId.get(d.id);
|
|
289
|
+
const r = plan.rows.get(d.id);
|
|
290
|
+
if (!t || !r) return [];
|
|
291
|
+
if (r.summary) return [{ id: d.id, start: isoOf(dayOf(d.start) + delta) }];
|
|
292
|
+
const span = spanOf(t);
|
|
293
|
+
if (d.mode === "move") {
|
|
294
|
+
if (r.milestone) {
|
|
295
|
+
const m = datesOf(CAL.lastIndex(dayOf(r.due) + delta) + 1, 0, true);
|
|
296
|
+
return [{ id: d.id, start: m.start, due: m.due }];
|
|
297
|
+
}
|
|
298
|
+
const m = datesOf(CAL.startIndex(dayOf(r.start) + delta), span.d, false);
|
|
299
|
+
return [{ id: d.id, start: m.start, due: m.due }];
|
|
300
|
+
}
|
|
301
|
+
if (d.mode === "end") return [{ id: d.id, start: r.start, due: isoOf(Math.max(dayOf(r.start), dayOf(r.due) + delta)) }];
|
|
302
|
+
return [{ id: d.id, start: isoOf(Math.min(dayOf(r.due), dayOf(r.start) + delta)), due: r.due }];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function previewFor(moves, extra = {}) {
|
|
306
|
+
const res = shiftDependents(tasks, links, moves, extra);
|
|
307
|
+
const dates = new Map(res.changes.map((c) => [String(c.id), { start: c.start_date, due: c.due_date }]));
|
|
308
|
+
const from = new Map(res.changes.filter((c) => c.from?.start_date).map((c) => [String(c.id), { start: c.from.start_date, due: c.from.due_date }]));
|
|
309
|
+
return { dates, from, plan: res.after, finishBefore: res.finishBefore, finishAfter: res.finishAfter, clamped: res.clamped, moves, changes: res.changes };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const describe = (p, id) => describeDrag({ before: plan, after: p.plan, changes: p.changes, clamped: p.clamped, id, titleOf: (x) => byId.get(x)?.title ?? "" });
|
|
313
|
+
|
|
314
|
+
function startDrag(e, id, modeName) {
|
|
315
|
+
if (!onMove) return;
|
|
316
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
317
|
+
const r = plan.rows.get(String(id));
|
|
318
|
+
if (!r?.dated || r.pinned) return;
|
|
319
|
+
if (e.pointerType !== "mouse" && modeName === "move" && !r.summary && !r.milestone) return;
|
|
320
|
+
e.stopPropagation();
|
|
321
|
+
const together = modeName === "move" && picked.size > 1 && picked.has(String(id)) ? [...picked].map(String) : null;
|
|
322
|
+
dragRef.current = { id: String(id), mode: modeName, x0: e.clientX, start: shown.get(String(id))?.start, delta: 0, moved: false, pointerId: e.pointerId, together };
|
|
323
|
+
try {
|
|
324
|
+
track.current?.setPointerCapture(e.pointerId);
|
|
325
|
+
} catch {
|
|
326
|
+
// the pointer is gone already
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function movesOfDrag(d, delta) {
|
|
331
|
+
if (!d.together) return movesFor(d, delta);
|
|
332
|
+
return selectionMoves(d.together, (id) => {
|
|
333
|
+
const r = plan.rows.get(id);
|
|
334
|
+
if (!r?.dated || r.pinned) return [];
|
|
335
|
+
return movesFor({ id, mode: "move", start: shown.get(id)?.start ?? r.start }, delta);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function onTrackMove(e) {
|
|
340
|
+
if (linkRef.current) return moveLink(e);
|
|
341
|
+
const d = dragRef.current;
|
|
342
|
+
if (!d || e.pointerId !== d.pointerId) return;
|
|
343
|
+
const dx = e.clientX - d.x0;
|
|
344
|
+
if (!d.moved && Math.abs(dx) < 4) return;
|
|
345
|
+
d.moved = true;
|
|
346
|
+
const delta = Math.round(dx / ppd);
|
|
347
|
+
if (delta === d.delta && preview) return;
|
|
348
|
+
d.delta = delta;
|
|
349
|
+
const p = previewFor(movesOfDrag(d, delta));
|
|
350
|
+
p.info = describe(p, d.id);
|
|
351
|
+
d.preview = p;
|
|
352
|
+
setPreview(p);
|
|
353
|
+
onPreview?.({ mode: d.mode === "end" ? "stretch" : "move", task: byId.get(d.id), ...p.info });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function onTrackUp(e) {
|
|
357
|
+
if (linkRef.current) return finishLink(e);
|
|
358
|
+
const d = dragRef.current;
|
|
359
|
+
if (!d || e.pointerId !== d.pointerId) return;
|
|
360
|
+
dragRef.current = null;
|
|
361
|
+
if (!d.moved) return;
|
|
362
|
+
suppress.current = true;
|
|
363
|
+
setTimeout(() => {
|
|
364
|
+
suppress.current = false;
|
|
365
|
+
}, 0);
|
|
366
|
+
const p = d.preview;
|
|
367
|
+
setPreview(null);
|
|
368
|
+
onPreview?.(null);
|
|
369
|
+
if (!p || d.delta === 0) return;
|
|
370
|
+
const res = await onMove?.(p.moves, p);
|
|
371
|
+
if (res && res.ok === false) setNote(res.error ?? "That move was not saved.");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function cancelAll() {
|
|
375
|
+
dragRef.current = null;
|
|
376
|
+
linkRef.current = null;
|
|
377
|
+
setPreview(null);
|
|
378
|
+
setLinking(null);
|
|
379
|
+
onPreview?.(null);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---- drawing links ----------------------------------------------------------
|
|
383
|
+
function trackPoint(e) {
|
|
384
|
+
const rect = track.current?.getBoundingClientRect();
|
|
385
|
+
return rect ? { x: e.clientX - rect.left, y: e.clientY - rect.top } : { x: 0, y: 0 };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function startLink(e, id, end) {
|
|
389
|
+
if (!onLink) return;
|
|
390
|
+
e.stopPropagation();
|
|
391
|
+
e.preventDefault();
|
|
392
|
+
const b = boxOf(id);
|
|
393
|
+
if (!b) return;
|
|
394
|
+
const from = { x: end === "start" ? b.x0 : b.x1, y: b.y };
|
|
395
|
+
const l = { fromId: String(id), fromEnd: end, from, to: from, targetId: null, targetEnd: null, problem: null, pointerId: e.pointerId, preview: null };
|
|
396
|
+
linkRef.current = l;
|
|
397
|
+
setLinking(l);
|
|
398
|
+
try {
|
|
399
|
+
track.current?.setPointerCapture(e.pointerId);
|
|
400
|
+
} catch {
|
|
401
|
+
// the pointer is gone already
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function moveLink(e) {
|
|
406
|
+
const cur = linkRef.current;
|
|
407
|
+
if (!cur || e.pointerId !== cur.pointerId) return;
|
|
408
|
+
const pt = trackPoint(e);
|
|
409
|
+
const entry = visible[rowAt(pt.y)];
|
|
410
|
+
let targetId = null;
|
|
411
|
+
let targetEnd = null;
|
|
412
|
+
let problem = null;
|
|
413
|
+
let p = null;
|
|
414
|
+
if (entry && entry.id !== cur.fromId) {
|
|
415
|
+
const b = boxOf(entry.id);
|
|
416
|
+
if (b && pt.x >= b.x0 - 12 && pt.x <= b.x1 + 12) {
|
|
417
|
+
targetId = entry.id;
|
|
418
|
+
targetEnd = b.milestone ? "start" : pt.x < (b.x0 + b.x1) / 2 ? "start" : "finish";
|
|
419
|
+
problem = linkProblem(tasks, links, targetId, cur.fromId);
|
|
420
|
+
if (!problem) {
|
|
421
|
+
p = previewFor([], { nextLinks: [...links, { id: "__new", task_id: targetId, depends_on_id: cur.fromId, kind: KIND_OF[`${cur.fromEnd}:${targetEnd}`], lag_days: 0 }] });
|
|
422
|
+
p.info = describe(p, targetId);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
linkRef.current = { ...cur, to: pt, targetId, targetEnd, problem, preview: p };
|
|
427
|
+
setLinking(linkRef.current);
|
|
428
|
+
setPreview(p);
|
|
429
|
+
onPreview?.(p ? { mode: "link", task: byId.get(targetId), ...p.info } : null);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function finishLink(e) {
|
|
433
|
+
const l = linkRef.current;
|
|
434
|
+
if (!l || e.pointerId !== l.pointerId) return;
|
|
435
|
+
linkRef.current = null;
|
|
436
|
+
setLinking(null);
|
|
437
|
+
const p = l.preview;
|
|
438
|
+
setPreview(null);
|
|
439
|
+
onPreview?.(null);
|
|
440
|
+
if (!l.targetId) return;
|
|
441
|
+
if (l.problem) return setNote(l.problem);
|
|
442
|
+
const res = await onLink?.({ taskId: l.targetId, dependsOnId: l.fromId, kind: KIND_OF[`${l.fromEnd}:${l.targetEnd}`] }, p);
|
|
443
|
+
if (res && res.ok === false) setNote(res.error ?? "That link was not saved.");
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
if (!preview && !linking) return undefined;
|
|
448
|
+
const onKey = (e) => {
|
|
449
|
+
if (e.key === "Escape") cancelAll();
|
|
450
|
+
};
|
|
451
|
+
window.addEventListener("keydown", onKey);
|
|
452
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
453
|
+
}, [preview, linking]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
454
|
+
|
|
455
|
+
// ---- keyboard on the chart ----------------------------------------------------
|
|
456
|
+
function onKeyDown(e) {
|
|
457
|
+
if (editing || e.target?.tagName === "INPUT") return;
|
|
458
|
+
const i = selected ? rowOf.get(selected) : null;
|
|
459
|
+
const task = selected ? byId.get(selected) : null;
|
|
460
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
461
|
+
e.preventDefault();
|
|
462
|
+
const next = i == null ? 0 : Math.max(0, Math.min(visible.length - 1, i + (e.key === "ArrowDown" ? 1 : -1)));
|
|
463
|
+
if (visible[next]) onSelect?.(visible[next].id);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (!task) return;
|
|
467
|
+
if (e.altKey && (e.key === "ArrowRight" || e.key === "ArrowLeft") && onMove && !e.shiftKey) {
|
|
468
|
+
e.preventDefault();
|
|
469
|
+
const r = plan.rows.get(String(task.id));
|
|
470
|
+
if (!r?.dated || r.pinned) return;
|
|
471
|
+
const step = e.key === "ArrowRight" ? 1 : -1;
|
|
472
|
+
const from = dayOf(r.milestone ? r.due : r.start);
|
|
473
|
+
const to = CAL.dayAt((r.milestone ? CAL.lastIndex(from) : CAL.startIndex(from)) + step);
|
|
474
|
+
const p = previewFor(movesFor({ id: String(task.id), mode: "move", start: r.start }, to - from));
|
|
475
|
+
onMove(p.moves, p).then((res) => res && res.ok === false && setNote(res.error ?? "That move was not saved."));
|
|
476
|
+
} else if (e.key === "Enter") {
|
|
477
|
+
e.preventDefault();
|
|
478
|
+
onOpen?.(task);
|
|
479
|
+
} else if (e.key === "F2") {
|
|
480
|
+
e.preventDefault();
|
|
481
|
+
setEditing({ id: String(task.id), field: "title" });
|
|
482
|
+
} else if (e.key === "Escape") onSelect?.(null);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function commitEdit(task, field, value) {
|
|
486
|
+
setEditing(null);
|
|
487
|
+
const res = await onEdit?.(task, field, value);
|
|
488
|
+
if (res && res.ok === false) setNote(res.error ?? "That change was not saved.");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function submitDraft(milestoneKey = false) {
|
|
492
|
+
const title = draft.title.trim();
|
|
493
|
+
if (!title || !onAdd || busy) return;
|
|
494
|
+
setBusy(true);
|
|
495
|
+
const milestone = draft.milestone || milestoneKey;
|
|
496
|
+
const res = await onAdd({ title, days: Number(draft.days) || null, milestone, parentId: draft.parentId, afterId: draft.afterId });
|
|
497
|
+
setBusy(false);
|
|
498
|
+
if (res && res.ok === false) setNote(res.error ?? "That task was not added.");
|
|
499
|
+
else setDraft((d) => ({ ...d, title: "", days: "", milestone: false, afterId: res?.id ? String(res.id) : d.afterId }));
|
|
500
|
+
addRef.current?.focus();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ---- the sheet's divider --------------------------------------------------------
|
|
504
|
+
function startDivider(e) {
|
|
505
|
+
e.preventDefault();
|
|
506
|
+
const x0 = e.clientX;
|
|
507
|
+
const w0 = gridW;
|
|
508
|
+
let last = w0;
|
|
509
|
+
const move = (ev) => {
|
|
510
|
+
last = Math.max(fixedWidth(cols) + 140, Math.min(760, w0 + ev.clientX - x0));
|
|
511
|
+
setGridState(last);
|
|
512
|
+
};
|
|
513
|
+
const up = () => {
|
|
514
|
+
window.removeEventListener("pointermove", move);
|
|
515
|
+
window.removeEventListener("pointerup", up);
|
|
516
|
+
onGridWidth?.(last);
|
|
517
|
+
};
|
|
518
|
+
window.addEventListener("pointermove", move);
|
|
519
|
+
window.addEventListener("pointerup", up);
|
|
520
|
+
}
|
|
521
|
+
const divider = narrow ? null : (
|
|
522
|
+
<div
|
|
523
|
+
role="separator"
|
|
524
|
+
aria-orientation="vertical"
|
|
525
|
+
aria-label="Resize the task sheet"
|
|
526
|
+
aria-valuenow={gridW}
|
|
527
|
+
tabIndex={0}
|
|
528
|
+
onPointerDown={startDivider}
|
|
529
|
+
onKeyDown={(e) => {
|
|
530
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
|
531
|
+
e.preventDefault();
|
|
532
|
+
setGridState(Math.max(fixedWidth(cols) + 140, Math.min(760, gridW + (e.key === "ArrowRight" ? 16 : -16))));
|
|
533
|
+
}
|
|
534
|
+
}}
|
|
535
|
+
className="absolute inset-y-0 -right-[3px] z-50 w-[6px] cursor-col-resize outline-none hover:bg-ring/30 focus-visible:bg-ring/40"
|
|
536
|
+
/>
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
// ---- render -------------------------------------------------------------------
|
|
540
|
+
const finish = preview?.finishAfter ?? plan.finish;
|
|
541
|
+
const gap = gapOf(finish, target);
|
|
542
|
+
const marks = {
|
|
543
|
+
today: showToday && inRange(today) ? X(today) + ppd / 2 : null,
|
|
544
|
+
target: target && inRange(target) ? X(target) + ppd : null,
|
|
545
|
+
targetIso: target,
|
|
546
|
+
finish: finish && inRange(finish) ? X(finish) + ppd : null,
|
|
547
|
+
finishFrom: preview && plan.finish && inRange(plan.finish) ? X(plan.finish) + ppd : null,
|
|
548
|
+
moving: Boolean(preview && !linking),
|
|
549
|
+
gap,
|
|
550
|
+
};
|
|
551
|
+
const bodyH = Math.max(visible.length, 1) * rowH + (addIndex != null ? ADD_H : 0);
|
|
552
|
+
const addAfter = addIndex != null && addIndex < visible.length - 1 ? addIndex : null;
|
|
553
|
+
const addParent = draft.parentId ? byId.get(String(draft.parentId)) : null;
|
|
554
|
+
const ghostMap = preview?.from?.size ? preview.from : ghosts;
|
|
555
|
+
const dragInfo = preview?.info ?? null;
|
|
556
|
+
const laneH = 20;
|
|
557
|
+
const laneHead = 28;
|
|
558
|
+
const hasLanes = Boolean(lanes?.length);
|
|
559
|
+
const lanesH = hasLanes ? laneHead + lanes.length * laneH : 0;
|
|
560
|
+
const rowTops = visible.map((_, i) => rowTop(i));
|
|
561
|
+
const selectedRows = new Set([...(selected ? [selected] : []), ...picked].map((id) => rowOf.get(String(id))).filter((i) => i != null));
|
|
562
|
+
const addAt = addIndex != null && addIndex < visible.length ? rowTop(addIndex) + rowH : bodyH;
|
|
563
|
+
|
|
564
|
+
const addRow = onAdd ? (
|
|
565
|
+
<AddRow
|
|
566
|
+
open={adding}
|
|
567
|
+
onOpen={() => {
|
|
568
|
+
setAdding(true);
|
|
569
|
+
setDraft({ title: "", days: "", milestone: false, parentId: null, afterId: null });
|
|
570
|
+
setTimeout(() => addRef.current?.focus(), 20);
|
|
571
|
+
}}
|
|
572
|
+
draft={draft}
|
|
573
|
+
setDraft={setDraft}
|
|
574
|
+
onSubmit={submitDraft}
|
|
575
|
+
busy={busy}
|
|
576
|
+
addRef={addRef}
|
|
577
|
+
parentTitle={addParent?.title}
|
|
578
|
+
aboveTitle={draft.afterId ? byId.get(draft.afterId)?.title : null}
|
|
579
|
+
onIndent={() => setDraft((d) => ({ ...d, parentId: d.afterId, afterId: null }))}
|
|
580
|
+
height={rowH}
|
|
581
|
+
/>
|
|
582
|
+
) : null;
|
|
583
|
+
const addExtra = onAdd && !adding ? rowH : 0;
|
|
584
|
+
|
|
585
|
+
const rootStyle = height === "fill" ? undefined : { height };
|
|
586
|
+
|
|
587
|
+
return (
|
|
588
|
+
<div className={cx("flex min-h-0 flex-col", height === "fill" && "h-full", className)} style={rootStyle}>
|
|
589
|
+
{error ? (
|
|
590
|
+
<p role="alert" className="shrink-0 border-b border-destructive px-3 py-2 text-[13px] text-destructive">
|
|
591
|
+
{typeof error === "string" ? error : "The plan could not be read."}
|
|
592
|
+
</p>
|
|
593
|
+
) : null}
|
|
594
|
+
{note ? (
|
|
595
|
+
<p role="alert" className="flex shrink-0 items-start justify-between gap-3 border-b border-border bg-card px-3 py-2 text-[13px]">
|
|
596
|
+
<span>{note}</span>
|
|
597
|
+
<button type="button" onClick={() => setNote(null)} className="shrink-0 text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring" aria-label="Dismiss the message">
|
|
598
|
+
<Icon name="x" size={16} />
|
|
599
|
+
</button>
|
|
600
|
+
</p>
|
|
601
|
+
) : null}
|
|
602
|
+
<div
|
|
603
|
+
ref={scroller}
|
|
604
|
+
role="region"
|
|
605
|
+
aria-label={label}
|
|
606
|
+
aria-busy={loading || undefined}
|
|
607
|
+
tabIndex={0}
|
|
608
|
+
onKeyDown={onKeyDown}
|
|
609
|
+
className="relative min-h-0 flex-1 overflow-auto bg-card outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
610
|
+
>
|
|
611
|
+
<div className="relative" style={{ width: gridW + trackW, minHeight: HEAD + bodyH + addExtra + lanesH }}>
|
|
612
|
+
<div className="sticky top-0 z-30 flex border-b border-border bg-background" style={{ height: HEAD }}>
|
|
613
|
+
<div className="sticky left-0 z-40 shrink-0 border-r border-border bg-background" style={{ width: gridW }}>
|
|
614
|
+
<SheetHead cols={cols} />
|
|
615
|
+
{divider}
|
|
616
|
+
</div>
|
|
617
|
+
<div className="relative" style={{ width: trackW }}>
|
|
618
|
+
<TrackHead axis={axis} range={range} ppd={ppd} z={z} gridW={gridW} marks={marks} />
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
|
|
622
|
+
<div className="flex">
|
|
623
|
+
<div className="sticky left-0 z-20 shrink-0 border-r border-border bg-card" style={{ width: gridW }}>
|
|
624
|
+
{loading
|
|
625
|
+
? Array.from({ length: 8 }, (_, i) => (
|
|
626
|
+
<div key={i} className="flex items-center gap-2 border-b border-border px-3" style={{ height: rowH }}>
|
|
627
|
+
<span className="h-2.5 w-8 animate-pulse rounded-[2px] bg-muted" />
|
|
628
|
+
<span className="h-2.5 flex-1 animate-pulse rounded-[2px] bg-muted" style={{ maxWidth: `${40 + ((i * 17) % 50)}%` }} />
|
|
629
|
+
</div>
|
|
630
|
+
))
|
|
631
|
+
: visible.map((e, i) => {
|
|
632
|
+
const t = byId.get(e.id);
|
|
633
|
+
const r = live.rows.get(e.id);
|
|
634
|
+
const d = shown.get(e.id);
|
|
635
|
+
const late = r && !r.summary && !r.pinned && r.dated && !r.milestone && d?.due && d.due < today && r.progress < 100;
|
|
636
|
+
const lateMs = r?.milestone && !r.pinned && d?.due && d.due < today;
|
|
637
|
+
return (
|
|
638
|
+
<div key={e.id}>
|
|
639
|
+
<SheetRow
|
|
640
|
+
e={e}
|
|
641
|
+
t={t}
|
|
642
|
+
r={r}
|
|
643
|
+
cols={cols}
|
|
644
|
+
rowH={rowH}
|
|
645
|
+
critical={critical}
|
|
646
|
+
selected={selected === e.id || picked.has(e.id)}
|
|
647
|
+
closed={closed.has(e.id)}
|
|
648
|
+
cells={cellsOf(e.id)}
|
|
649
|
+
foldedCount={r?.summary ? `${r.leafIds.length} ${r.leafIds.length === 1 ? "task" : "tasks"}` : null}
|
|
650
|
+
tags={tags?.get?.(e.id)}
|
|
651
|
+
late={late || lateMs}
|
|
652
|
+
dim={highlight && !highlight.has(e.id) && !r?.summary}
|
|
653
|
+
moving={moving}
|
|
654
|
+
editing={editing?.id === e.id ? editing : null}
|
|
655
|
+
onSelect={onSelect}
|
|
656
|
+
onOpen={onOpen}
|
|
657
|
+
onToggle={onToggle}
|
|
658
|
+
onOpenLink={onOpenLink}
|
|
659
|
+
onStartEdit={(field) => {
|
|
660
|
+
onSelect?.(e.id);
|
|
661
|
+
setEditing({ id: e.id, field });
|
|
662
|
+
}}
|
|
663
|
+
onCommit={(field, v) => commitEdit(t, field, v)}
|
|
664
|
+
onCancel={() => setEditing(null)}
|
|
665
|
+
onHover={() => setHovered(e.id)}
|
|
666
|
+
menu={rowMenu && (hovered === e.id || selected === e.id) ? rowMenu(t, { row: e, plan: r }) : null}
|
|
667
|
+
/>
|
|
668
|
+
{addAfter === i && adding ? addRow : null}
|
|
669
|
+
</div>
|
|
670
|
+
);
|
|
671
|
+
})}
|
|
672
|
+
{addAfter == null ? addRow : null}
|
|
673
|
+
{hasLanes ? (
|
|
674
|
+
<div>
|
|
675
|
+
<div className="flex items-center gap-2 border-b border-t border-border bg-muted px-3 text-[12px] font-semibold" style={{ height: laneHead }}>
|
|
676
|
+
{lanesTitle}
|
|
677
|
+
</div>
|
|
678
|
+
<LaneLabels lanes={lanes} laneH={laneH} width={gridW - 1} />
|
|
679
|
+
</div>
|
|
680
|
+
) : null}
|
|
681
|
+
</div>
|
|
682
|
+
|
|
683
|
+
<div
|
|
684
|
+
ref={track}
|
|
685
|
+
className={cx("relative", linking && "cursor-crosshair")}
|
|
686
|
+
style={{ width: trackW, height: bodyH + addExtra + lanesH, touchAction: dragRef.current || linking ? "none" : "pan-x pan-y" }}
|
|
687
|
+
onPointerMove={onTrackMove}
|
|
688
|
+
onPointerUp={onTrackUp}
|
|
689
|
+
onPointerCancel={cancelAll}
|
|
690
|
+
>
|
|
691
|
+
<Paper axis={axis} range={range} ppd={ppd} z={z} marks={marks} rowTops={rowTops} rowH={rowH} selectedRows={selectedRows} />
|
|
692
|
+
<Links lines={lines} width={trackW} height={bodyH} critical={critical} onOpenLink={onOpenLink} />
|
|
693
|
+
<Ghosts ghosts={ghostMap} visible={visible} X={X} ppd={ppd} rowH={rowH} rowTop={rowTop} live={live} kind="was" bar={BAR} />
|
|
694
|
+
<Ghosts ghosts={pencil} visible={visible} X={X} ppd={ppd} rowH={rowH} rowTop={rowTop} live={live} kind="pencil" bar={BAR} />
|
|
695
|
+
{addIndex != null && !draft.milestone ? (
|
|
696
|
+
<span aria-hidden="true" className="absolute h-3.5 rounded-[2px] border border-dashed border-info bg-info/15" style={{ left: today ? X(today) : 0, width: Math.max(1, Number(draft.days) || 3) * ppd * 1.4, top: rowTop(addIndex) + rowH + (ADD_H - 14) / 2 }} />
|
|
697
|
+
) : null}
|
|
698
|
+
|
|
699
|
+
{visible.map((e, i) => {
|
|
700
|
+
const t = byId.get(e.id);
|
|
701
|
+
const r = live.rows.get(e.id);
|
|
702
|
+
const d = shown.get(e.id);
|
|
703
|
+
const y = rowTop(i) + rowH / 2;
|
|
704
|
+
const n = notes?.get?.(e.id);
|
|
705
|
+
if (!r || !d) {
|
|
706
|
+
return n ? (
|
|
707
|
+
<span key={e.id} className={cx("absolute -translate-y-1/2 whitespace-nowrap text-[12px]", n.tone === "problem" ? "font-medium text-destructive" : "text-muted-foreground")} style={{ left: 12, top: y }}>
|
|
708
|
+
{n.text}
|
|
709
|
+
</span>
|
|
710
|
+
) : null;
|
|
711
|
+
}
|
|
712
|
+
const isSel = selected === e.id || picked.has(e.id);
|
|
713
|
+
const dim = highlight && !highlight.has(e.id) && !r.summary;
|
|
714
|
+
const done = r.pinned;
|
|
715
|
+
const late = !r.summary && !done && !r.milestone && d.due < today && r.progress < 100;
|
|
716
|
+
const title = barLabel(t, r);
|
|
717
|
+
const common = {
|
|
718
|
+
tabIndex: -1,
|
|
719
|
+
onClick: (ev) => {
|
|
720
|
+
ev.stopPropagation();
|
|
721
|
+
if (suppress.current) return;
|
|
722
|
+
onSelect?.(e.id, { additive: ev.metaKey || ev.ctrlKey || ev.shiftKey });
|
|
723
|
+
},
|
|
724
|
+
onDoubleClick: () => onOpen?.(t),
|
|
725
|
+
title,
|
|
726
|
+
"aria-label": title,
|
|
727
|
+
};
|
|
728
|
+
const crit = critical && r.critical && !done;
|
|
729
|
+
const labelEl = (x, extra = 0) => {
|
|
730
|
+
const person = people?.get?.(String(t.assignee_id ?? ""));
|
|
731
|
+
const who = firstName(person?.name ?? t.assignee_name);
|
|
732
|
+
const text = n?.text ?? who;
|
|
733
|
+
return text || late ? (
|
|
734
|
+
<span className="pointer-events-none absolute top-1/2 flex -translate-y-1/2 items-center gap-1.5 whitespace-nowrap text-[11px]" style={{ left: x + 6 + extra }}>
|
|
735
|
+
{late ? <span className="rounded-[3px] border border-warning px-1 text-[10px] font-semibold leading-4 text-warning">Late</span> : null}
|
|
736
|
+
{text ? <span className={n?.tone === "problem" ? "font-medium text-destructive" : n?.tone === "crit" ? "font-medium text-destructive" : "text-muted-foreground"}>{text}</span> : null}
|
|
737
|
+
</span>
|
|
738
|
+
) : null;
|
|
739
|
+
};
|
|
740
|
+
const handles =
|
|
741
|
+
onLink && !r.summary && !done ? (
|
|
742
|
+
<>
|
|
743
|
+
<button type="button" tabIndex={-1} aria-label={`Link from the start of ${t.title}`} onPointerDown={(ev) => startLink(ev, e.id, "start")} className={cx("absolute top-1/2 z-20 size-2 -translate-y-1/2 rounded-full border-2 border-ring bg-background opacity-0 outline-none group-hover:opacity-100", isSel && "opacity-100")} style={{ left: -11, touchAction: "none" }} />
|
|
744
|
+
<button type="button" tabIndex={-1} aria-label={`Link from the end of ${t.title}`} onPointerDown={(ev) => startLink(ev, e.id, "finish")} className={cx("absolute top-1/2 z-20 size-2 -translate-y-1/2 rounded-full border-2 border-ring bg-background opacity-0 outline-none group-hover:opacity-100", isSel && "opacity-100")} style={{ right: -11, touchAction: "none" }} />
|
|
745
|
+
</>
|
|
746
|
+
) : null;
|
|
747
|
+
const target = linking?.targetId === e.id;
|
|
748
|
+
|
|
749
|
+
if (r.milestone) {
|
|
750
|
+
const cx0 = X(d.due) + ppd / 2;
|
|
751
|
+
return (
|
|
752
|
+
<div key={e.id} className={cx("group absolute z-10", dim && "opacity-40")} style={{ left: cx0 - 9, top: y - 9, width: 18, height: 18 }}>
|
|
753
|
+
<button type="button" {...common} onPointerDown={(ev) => startDrag(ev, e.id, "move")} className={cx("absolute inset-0 grid place-items-center rounded-[2px] outline-none focus-visible:ring-2 focus-visible:ring-ring", onMove && !done && "cursor-grab", target && (linking.problem ? "ring-2 ring-destructive" : "ring-2 ring-ring"))}>
|
|
754
|
+
<span className={cx("block size-3 rotate-45", done ? "bg-chart-2" : crit ? "bg-destructive" : "bg-foreground", isSel && "ring-2 ring-ring ring-offset-1 ring-offset-card")} />
|
|
755
|
+
</button>
|
|
756
|
+
{handles}
|
|
757
|
+
<span className={cx(NUM, "pointer-events-none absolute left-full top-1/2 ml-2 -translate-y-1/2 whitespace-nowrap text-[11px]", crit ? "font-semibold text-destructive" : "text-muted-foreground")}>{n?.text ?? dayWords(d.due)}</span>
|
|
758
|
+
</div>
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const x0 = X(d.start);
|
|
763
|
+
const w = Math.max(ppd, X(d.due) + ppd - x0);
|
|
764
|
+
if (r.summary) {
|
|
765
|
+
const folded = closed.has(e.id);
|
|
766
|
+
const kids = folded ? r.leafIds.map((id) => ({ id, row: live.rows.get(id), d: shown.get(id) })).filter((k) => k.row?.milestone && k.d) : [];
|
|
767
|
+
return (
|
|
768
|
+
<div key={e.id} className="group absolute z-10" style={{ left: x0, top: y - 4, width: w, height: 12 }}>
|
|
769
|
+
<button type="button" {...common} onPointerDown={(ev) => startDrag(ev, e.id, "move")} className={cx("absolute inset-x-0 top-0 h-[6px] outline-none focus-visible:ring-2 focus-visible:ring-ring", done ? "bg-chart-2" : "bg-foreground", onMove && !done && "cursor-grab")}>
|
|
770
|
+
<span className={cx("absolute left-0 top-[6px] size-0 border-r-[5px] border-t-[5px] border-r-transparent", done ? "border-t-chart-2" : "border-t-foreground")} />
|
|
771
|
+
<span className={cx("absolute right-0 top-[6px] size-0 border-l-[5px] border-t-[5px] border-l-transparent", done ? "border-t-chart-2" : "border-t-foreground")} />
|
|
772
|
+
</button>
|
|
773
|
+
{kids.map((k) => (
|
|
774
|
+
<span key={k.id} title={byId.get(k.id)?.title} className={cx("absolute -top-[10px] size-[10px] -translate-x-1/2 rotate-45 border-[1.5px] border-card", k.row.pinned ? "bg-chart-2" : critical && k.row.critical ? "bg-destructive" : "bg-foreground")} style={{ left: X(k.d.due) + ppd / 2 - x0 }} />
|
|
775
|
+
))}
|
|
776
|
+
{n ? (
|
|
777
|
+
<span className="pointer-events-none absolute -top-[4px] whitespace-nowrap text-[11px] text-muted-foreground" style={{ left: w + 8 }}>
|
|
778
|
+
{n.text}
|
|
779
|
+
</span>
|
|
780
|
+
) : null}
|
|
781
|
+
</div>
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
const loop = r.stuck;
|
|
785
|
+
const guess = guesses?.has?.(e.id);
|
|
786
|
+
const barTop = y - BAR / 2 - (baseline ? 2 : 0);
|
|
787
|
+
const base = baseline?.get?.(e.id);
|
|
788
|
+
const dragging = preview && dragRef.current?.id === e.id;
|
|
789
|
+
const movedByDrag = preview?.dates?.has(e.id);
|
|
790
|
+
const trackClass = loop ? "border border-dashed border-destructive bg-destructive/15" : guess ? "border border-dashed border-chart-2 bg-transparent" : crit ? "bg-destructive" : done ? "bg-chart-2" : "bg-chart-1";
|
|
791
|
+
const fill = crit ? "bg-foreground/35" : "bg-chart-2";
|
|
792
|
+
const mark = tags?.get?.(e.id)?.some((g) => g.kind === "unsaved");
|
|
793
|
+
return (
|
|
794
|
+
<div key={e.id} className={cx("group absolute z-10", dim && "opacity-40")} style={{ left: x0, top: barTop, width: w, height: BAR }}>
|
|
795
|
+
<button
|
|
796
|
+
type="button"
|
|
797
|
+
{...common}
|
|
798
|
+
onPointerDown={(ev) => startDrag(ev, e.id, "move")}
|
|
799
|
+
className={cx(
|
|
800
|
+
"absolute inset-0 overflow-hidden rounded-[2px] outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
801
|
+
trackClass,
|
|
802
|
+
isSel && "ring-2 ring-ring",
|
|
803
|
+
(dragging || movedByDrag) && !isSel && "ring-[1.5px] ring-ring",
|
|
804
|
+
mark && "ring-2 ring-destructive",
|
|
805
|
+
target && (linking.problem ? "ring-2 ring-destructive" : "ring-2 ring-ring"),
|
|
806
|
+
onMove && !done && "cursor-grab",
|
|
807
|
+
)}
|
|
808
|
+
>
|
|
809
|
+
{loop ? <span className="absolute inset-0 opacity-40" style={{ backgroundImage: "repeating-linear-gradient(135deg, currentColor 0 2px, transparent 2px 6px)" }} /> : null}
|
|
810
|
+
{!loop && !guess && !done && r.progress > 0 ? <span className={cx("absolute inset-y-0 left-0", fill)} style={{ width: `${r.progress}%` }} /> : null}
|
|
811
|
+
</button>
|
|
812
|
+
{base ? <span aria-hidden="true" className="pointer-events-none absolute h-1 rounded-[1px] bg-chart-3" style={{ left: X(base.start) - x0, width: Math.max(ppd, X(base.due) + ppd - X(base.start)), top: BAR + 2 }} /> : null}
|
|
813
|
+
{onMove && !done ? (
|
|
814
|
+
<>
|
|
815
|
+
<span onPointerDown={(ev) => startDrag(ev, e.id, "start")} aria-hidden="true" className={cx("absolute z-10 cursor-ew-resize", dragging || isSel ? "-inset-y-[2px] -left-[2px] w-1.5 rounded-[2px] bg-ring" : "inset-y-0 left-0 w-2")} style={{ touchAction: "none" }} />
|
|
816
|
+
<span onPointerDown={(ev) => startDrag(ev, e.id, "end")} aria-hidden="true" className={cx("absolute z-10 cursor-ew-resize", dragging || isSel ? "-inset-y-[2px] -right-[2px] w-1.5 rounded-[2px] bg-ring" : "inset-y-0 right-0 w-2")} style={{ touchAction: "none" }} />
|
|
817
|
+
</>
|
|
818
|
+
) : null}
|
|
819
|
+
{handles}
|
|
820
|
+
{labelEl(w, isSel ? 8 : 0)}
|
|
821
|
+
</div>
|
|
822
|
+
);
|
|
823
|
+
})}
|
|
824
|
+
|
|
825
|
+
{dragInfo && dragRef.current ? <DragTip info={dragInfo} task={byId.get(dragInfo.id)} box={boxOf(dragInfo.id)} tasks={tasks} plan={plan} /> : null}
|
|
826
|
+
|
|
827
|
+
{linking ? (
|
|
828
|
+
(() => {
|
|
829
|
+
const dx = linking.to.x - linking.from.x;
|
|
830
|
+
const dy = linking.to.y - linking.from.y;
|
|
831
|
+
const lines2 = linking.targetId
|
|
832
|
+
? linkLines({ task: byId.get(linking.targetId), predecessor: byId.get(linking.fromId), kind: KIND_OF[`${linking.fromEnd}:${linking.targetEnd}`], info: linking.preview?.info, problem: linking.problem })
|
|
833
|
+
: null;
|
|
834
|
+
return (
|
|
835
|
+
<>
|
|
836
|
+
<span aria-hidden="true" className={cx("pointer-events-none absolute z-30 h-0 origin-left border-t-[1.5px] border-dashed", linking.problem ? "border-destructive" : "border-ring")} style={{ left: linking.from.x, top: linking.from.y, width: Math.sqrt(dx * dx + dy * dy), transform: `rotate(${(Math.atan2(dy, dx) * 180) / Math.PI}deg)` }} />
|
|
837
|
+
{lines2 ? (
|
|
838
|
+
<div role="status" className="pointer-events-none absolute z-40 max-w-80 rounded-[4px] border border-border bg-popover px-2.5 py-2 text-[12px] leading-snug text-popover-foreground shadow-lg" style={{ left: linking.to.x + 12, top: linking.to.y + 10 }}>
|
|
839
|
+
<div className={lines2.tone === "problem" ? "font-medium text-destructive" : undefined}>{lines2.text}</div>
|
|
840
|
+
{lines2.tail ? <div className={lines2.tone === "late" ? "font-semibold text-warning" : "text-muted-foreground"}>{lines2.tail}</div> : null}
|
|
841
|
+
</div>
|
|
842
|
+
) : null}
|
|
843
|
+
</>
|
|
844
|
+
);
|
|
845
|
+
})()
|
|
846
|
+
) : null}
|
|
847
|
+
|
|
848
|
+
{visible.length === 0 && !loading ? (
|
|
849
|
+
<div className="absolute inset-x-0 top-6 grid place-items-center px-6 text-center">
|
|
850
|
+
<p className="max-w-sm text-[13px] text-muted-foreground">{emptyText}</p>
|
|
851
|
+
</div>
|
|
852
|
+
) : null}
|
|
853
|
+
|
|
854
|
+
{hasLanes ? <LaneTrack lanes={lanes} X={X} ppd={ppd} laneH={laneH} top={bodyH + addExtra + laneHead} /> : null}
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
</div>
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/** The note by a bar while it moves: what it is, where it ends, what it costs, in the canvas's words. */
|
|
864
|
+
function DragTip({ info, task, box, tasks, plan }) {
|
|
865
|
+
if (!box || !task) return null;
|
|
866
|
+
const next = (() => {
|
|
867
|
+
const l = [...plan.links.values()].find((x) => x.valid && x.dependsOnId === info.id);
|
|
868
|
+
return l ? tasks.find((t) => String(t.id) === l.taskId)?.title : null;
|
|
869
|
+
})();
|
|
870
|
+
const lines = dragLines(info, task, { nextTitle: next });
|
|
871
|
+
return (
|
|
872
|
+
<div role="status" className="pointer-events-none absolute z-40 grid w-max max-w-80 gap-0.5 rounded-[4px] border border-border bg-popover px-2.5 py-2 text-[12px] leading-snug text-popover-foreground shadow-lg" style={{ left: box.x1 + 14, top: Math.max(4, box.y - 64) }}>
|
|
873
|
+
<span className="text-[13px] font-semibold">{lines.title}</span>
|
|
874
|
+
<span className={NUM}>{lines.first}</span>
|
|
875
|
+
<span>{lines.second}</span>
|
|
876
|
+
{lines.slack ? <span className="text-muted-foreground">{lines.slack}</span> : null}
|
|
877
|
+
<span className={cx(NUM, lines.third.tone === "late" ? "font-semibold text-warning" : lines.third.tone === "early" ? "font-semibold text-success" : "text-muted-foreground")}>{lines.third.text}</span>
|
|
878
|
+
<span className="text-muted-foreground">{lines.last}</span>
|
|
879
|
+
</div>
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|