makersclaw 0.8.0 → 0.8.2
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 +234 -0
- package/registry/blocks/agent-pane/agent-pane.jsx +246 -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 +1677 -8
- package/templates/next/files/lib/platform.js +529 -21
- package/templates/next/template.json +13 -3
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button, Empty, Icon, Notice, Skeleton, cx } from "../../ui";
|
|
4
|
+
import {
|
|
5
|
+
dayName,
|
|
6
|
+
dayNumber,
|
|
7
|
+
gapOf,
|
|
8
|
+
gapWords,
|
|
9
|
+
gateOf,
|
|
10
|
+
axisOf,
|
|
11
|
+
frameOf,
|
|
12
|
+
labelPct,
|
|
13
|
+
layoutLanes,
|
|
14
|
+
placeLabel,
|
|
15
|
+
readMilestones,
|
|
16
|
+
readSegments,
|
|
17
|
+
shortDay,
|
|
18
|
+
summaryOf,
|
|
19
|
+
} from "./ruler-model";
|
|
20
|
+
|
|
21
|
+
// ScheduleRuler: a dated chain drawn to scale, like a surveyor's chain.
|
|
22
|
+
// A baseline of days (a tick each Monday, a taller one on the 1st); the
|
|
23
|
+
// segments of the chain as bars with their names; a gate post at the end of
|
|
24
|
+
// a segment, built from its exit criteria as pips; milestone diamonds; the
|
|
25
|
+
// target as a solid line and the forecast as a dashed one, and between them
|
|
26
|
+
// the gap hatched: green when the forecast lands early (slack), red when it
|
|
27
|
+
// lands late (an overrun), with its size in working days ("+3 d"). Today is
|
|
28
|
+
// a line in the accent.
|
|
29
|
+
//
|
|
30
|
+
// GapRuler is the target against the forecast and nothing else, for a
|
|
31
|
+
// sentence to sit beside. ScheduleStrip is the chain small, one row, for a
|
|
32
|
+
// row in a list.
|
|
33
|
+
//
|
|
34
|
+
// Positions are percentages of the chain's width: nothing measures the page,
|
|
35
|
+
// so the server's HTML and the browser agree. The chain is never narrower
|
|
36
|
+
// than `minWidth`; a container narrower than that scrolls it sideways and,
|
|
37
|
+
// under 36rem of its own width, also lists the chain in words below it.
|
|
38
|
+
// Props in, one callback out (`onOpen`); it imports only React and the kit.
|
|
39
|
+
// README.md beside this file lists every prop.
|
|
40
|
+
|
|
41
|
+
const AXIS = 30;
|
|
42
|
+
const MILE_H = 20;
|
|
43
|
+
const GHOST_H = 14;
|
|
44
|
+
const GATE_H = 38;
|
|
45
|
+
const FLAG_H = 22;
|
|
46
|
+
|
|
47
|
+
// The hatch is drawn in currentColor, so a text-* token paints it.
|
|
48
|
+
const HATCH = { backgroundImage: "repeating-linear-gradient(135deg, currentColor 0 1.5px, transparent 1.5px 7px)" };
|
|
49
|
+
const HATCH_FINE = { backgroundImage: "repeating-linear-gradient(135deg, currentColor 0 1.2px, transparent 1.2px 5px)" };
|
|
50
|
+
|
|
51
|
+
// A segment's status: done (solid, success) · active (solid ink) · planned
|
|
52
|
+
// (an ink outline) · late (solid accent: past its gate) · draft (dashed,
|
|
53
|
+
// breathing: still being written).
|
|
54
|
+
const TONE = {
|
|
55
|
+
done: "bg-success text-background border border-success",
|
|
56
|
+
active: "bg-foreground text-background border border-foreground",
|
|
57
|
+
planned: "bg-background text-foreground border border-foreground",
|
|
58
|
+
late: "bg-primary text-primary-foreground border border-primary",
|
|
59
|
+
draft: "border-[1.5px] border-dashed border-muted-foreground bg-background text-muted-foreground motion-safe:animate-pulse",
|
|
60
|
+
};
|
|
61
|
+
const STRIP = {
|
|
62
|
+
done: "bg-success",
|
|
63
|
+
active: "bg-foreground",
|
|
64
|
+
late: "bg-primary",
|
|
65
|
+
planned: "border border-foreground bg-background",
|
|
66
|
+
draft: "border border-dashed border-muted-foreground",
|
|
67
|
+
};
|
|
68
|
+
const STATUS_WORDS = { done: "done", active: "in progress", planned: "planned", late: "late", draft: "draft" };
|
|
69
|
+
|
|
70
|
+
const GAP_TEXT = { over: "text-destructive", slack: "text-success", on: "text-foreground" };
|
|
71
|
+
const GAP_LINE = { over: "border-destructive", slack: "border-success", on: "border-foreground" };
|
|
72
|
+
|
|
73
|
+
const WORDS = {
|
|
74
|
+
target: "Target",
|
|
75
|
+
forecast: "Forecast",
|
|
76
|
+
today: "Today",
|
|
77
|
+
on: "On target",
|
|
78
|
+
over: (n) => `+${n} d`,
|
|
79
|
+
slack: (n) => `${n} d slack`,
|
|
80
|
+
undated: (n) => `${n} without dates, left off`,
|
|
81
|
+
early: "Earlier plan",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const pct = (n) => `${n}%`;
|
|
85
|
+
|
|
86
|
+
/** The things every form of the ruler reads the same way. */
|
|
87
|
+
function readProps({ segments, milestones, target, forecast, today, count, workweek }) {
|
|
88
|
+
const { segs, skipped } = readSegments(segments);
|
|
89
|
+
const miles = readMilestones(milestones);
|
|
90
|
+
const tgt = dayNumber(target);
|
|
91
|
+
const t = dayNumber(today);
|
|
92
|
+
// forecast: undefined → the chain's own last day; null or "" → none.
|
|
93
|
+
const derived = forecast === undefined && segs.length ? Math.max(...segs.map((p) => p.e)) : null;
|
|
94
|
+
const fc = forecast === undefined ? derived : dayNumber(forecast);
|
|
95
|
+
const gap = gapOf({ target: tgt, forecast: fc, count, workweek });
|
|
96
|
+
return { segs, skipped, miles, tgt, fc, t, gap };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The full chain.
|
|
101
|
+
*
|
|
102
|
+
* Props: see README.md. In short: `segments` [{ id, label, start, end,
|
|
103
|
+
* status, sub, gate }], `milestones` [{ id, date, label, done }], `target`,
|
|
104
|
+
* `forecast`, `today`, `ghost`, `count`, `words`, `compact`, `minWidth`,
|
|
105
|
+
* `onOpen`, `loading`, `error`, `onRetry`, `empty`, `emptyAction`.
|
|
106
|
+
*/
|
|
107
|
+
export function ScheduleRuler({
|
|
108
|
+
segments,
|
|
109
|
+
milestones,
|
|
110
|
+
start,
|
|
111
|
+
target,
|
|
112
|
+
forecast,
|
|
113
|
+
today,
|
|
114
|
+
ghost,
|
|
115
|
+
count = "working",
|
|
116
|
+
workweek,
|
|
117
|
+
words: wordsIn,
|
|
118
|
+
compact = false,
|
|
119
|
+
minWidth = 560,
|
|
120
|
+
fitToday = true,
|
|
121
|
+
from,
|
|
122
|
+
to,
|
|
123
|
+
onOpen,
|
|
124
|
+
loading = false,
|
|
125
|
+
error,
|
|
126
|
+
onRetry,
|
|
127
|
+
empty,
|
|
128
|
+
emptyAction,
|
|
129
|
+
label = "Schedule",
|
|
130
|
+
className,
|
|
131
|
+
}) {
|
|
132
|
+
const words = { ...WORDS, ...wordsIn };
|
|
133
|
+
|
|
134
|
+
if (loading) {
|
|
135
|
+
return (
|
|
136
|
+
<div aria-busy="true" aria-label={`${label}, loading`} className={cx("@container grid min-w-0 gap-3", className)}>
|
|
137
|
+
<Skeleton shape="rect" className="h-4 w-full" />
|
|
138
|
+
<Skeleton shape="rect" className="h-12 w-full" />
|
|
139
|
+
<Skeleton shape="rect" className="h-4 w-2/3" />
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (error) {
|
|
144
|
+
return (
|
|
145
|
+
<div className={cx("@container min-w-0", className)}>
|
|
146
|
+
<Notice
|
|
147
|
+
tone="danger"
|
|
148
|
+
title={`${label} did not load`}
|
|
149
|
+
action={onRetry ? <Button size="sm" onClick={onRetry}>Try again</Button> : null}
|
|
150
|
+
>
|
|
151
|
+
{typeof error === "string" ? error : "Something went wrong reading the dates."}
|
|
152
|
+
</Notice>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const { segs, skipped, miles, tgt, fc, t, gap } = readProps({ segments, milestones, target, forecast, today, count, workweek });
|
|
158
|
+
const startDay = dayNumber(start);
|
|
159
|
+
const ghosts = (ghost ?? [])
|
|
160
|
+
.map((g) => ({ s: dayNumber(g?.start), e: dayNumber(g?.end) }))
|
|
161
|
+
.filter((g) => g.s != null && g.e != null && g.e >= g.s);
|
|
162
|
+
const days = [
|
|
163
|
+
startDay,
|
|
164
|
+
...segs.flatMap((p) => [p.s, p.e + 1]),
|
|
165
|
+
...miles.flatMap((m) => [m.day, m.day + 1]),
|
|
166
|
+
tgt != null ? tgt + 1 : null,
|
|
167
|
+
fc != null ? fc + 1 : null,
|
|
168
|
+
...ghosts.flatMap((g) => [g.s, g.e + 1]),
|
|
169
|
+
fitToday && t != null ? t : null,
|
|
170
|
+
].filter((d) => d != null);
|
|
171
|
+
const frame = segs.length || miles.length || tgt != null ? frameOf({ days, from: dayNumber(from), to: dayNumber(to) }) : null;
|
|
172
|
+
|
|
173
|
+
if (!frame) {
|
|
174
|
+
return (
|
|
175
|
+
<div className={cx("@container min-w-0", className)}>
|
|
176
|
+
<Empty
|
|
177
|
+
icon="calendar"
|
|
178
|
+
title={empty ?? "Nothing is dated yet"}
|
|
179
|
+
body={skipped ? words.undated(skipped) : null}
|
|
180
|
+
action={emptyAction}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const { r0, r1, x } = frame;
|
|
187
|
+
const axis = axisOf(r0, r1);
|
|
188
|
+
const inWindow = (d) => d != null && d >= r0 && d <= r1;
|
|
189
|
+
const tIn = inWindow(t) ? t : null;
|
|
190
|
+
|
|
191
|
+
// ---- rows, top to bottom, in px
|
|
192
|
+
const mileItems = miles.map((m) => {
|
|
193
|
+
const text = `${m.label} ${shortDay(m.day)}`;
|
|
194
|
+
const w = labelPct(text.length, minWidth, 26);
|
|
195
|
+
const px = x(m.day) + (100 / (r1 - r0)) / 2;
|
|
196
|
+
return { id: m.id, m, px, ...placeLabel({ x: px, width: w, prefer: m.side ?? "right" }) };
|
|
197
|
+
});
|
|
198
|
+
const mileLay = layoutLanes(mileItems);
|
|
199
|
+
const mileZone = miles.length ? mileLay.count * MILE_H + 6 : 0;
|
|
200
|
+
const ghostZone = ghosts.length ? GHOST_H + 4 : 0;
|
|
201
|
+
const bandTop = AXIS + mileZone + ghostZone + 8;
|
|
202
|
+
const bandH = compact ? 36 : 48;
|
|
203
|
+
const bandBottom = bandTop + bandH;
|
|
204
|
+
|
|
205
|
+
const gateItems = compact
|
|
206
|
+
? []
|
|
207
|
+
: segs
|
|
208
|
+
.map((p) => ({ p, g: gateOf(p.gate, p.e, t) }))
|
|
209
|
+
.filter((r) => r.g)
|
|
210
|
+
.map(({ p, g }) => {
|
|
211
|
+
const gx = x(g.day + 1);
|
|
212
|
+
// The date and the count share a line; the pips (12px each) are the other.
|
|
213
|
+
const chars = Math.max(shortDay(g.day).length + (g.total ? 5 : 0), g.total * 1.7);
|
|
214
|
+
return { id: p.id, p, g, gx, ...placeLabel({ x: gx, width: labelPct(chars, minWidth, 8), prefer: "left" }) };
|
|
215
|
+
});
|
|
216
|
+
const gateLay = layoutLanes(gateItems);
|
|
217
|
+
const gateZone = gateItems.length ? 8 + gateLay.count * GATE_H : 0;
|
|
218
|
+
|
|
219
|
+
const gapTop = bandBottom + gateZone + 4;
|
|
220
|
+
const gapRow = gap ? 20 : 0;
|
|
221
|
+
|
|
222
|
+
const flags = [];
|
|
223
|
+
if (tgt != null) flags.push({ id: "target", day: tgt + 1, text: `${words.target} ${dayName(tgt)}`, kind: "target" });
|
|
224
|
+
if (fc != null && gap?.state !== "on" && (tgt != null || forecast !== undefined)) flags.push({ id: "forecast", day: fc + 1, text: `${words.forecast} ${dayName(fc)}`, kind: "forecast" });
|
|
225
|
+
if (tIn != null) flags.push({ id: "today", day: tIn, text: `${words.today} ${shortDay(tIn)}`, kind: "today" });
|
|
226
|
+
const flagItems = compact
|
|
227
|
+
? []
|
|
228
|
+
: flags.map((f) => {
|
|
229
|
+
const fx = x(f.day);
|
|
230
|
+
return { ...f, fx, ...placeLabel({ x: fx, width: labelPct(f.text.length, minWidth, 18), prefer: "right" }) };
|
|
231
|
+
});
|
|
232
|
+
const flagLay = layoutLanes(flagItems);
|
|
233
|
+
const flagTop = gapTop + gapRow + 6;
|
|
234
|
+
const flagZone = flagItems.length ? flagLay.count * FLAG_H : 0;
|
|
235
|
+
const height = (compact ? bandBottom + 6 + gapRow : flagTop + flagZone) + 4;
|
|
236
|
+
const bottomOf = (lane) => flagTop + lane * FLAG_H + 18;
|
|
237
|
+
// A line runs down to its own flag; with no flag drawn (compact) it stops under the band.
|
|
238
|
+
const lineBottom = (id) => (flagLay.lanes[id] != null ? bottomOf(flagLay.lanes[id]) : bandBottom + 8);
|
|
239
|
+
|
|
240
|
+
const summary = summaryOf({
|
|
241
|
+
label,
|
|
242
|
+
segs: segs.map((p) => ({ ...p, status: STATUS_WORDS[p.status] ?? p.status, gate: gateOf(p.gate, p.e, t) })),
|
|
243
|
+
milestones: miles,
|
|
244
|
+
target: tgt,
|
|
245
|
+
forecast: fc,
|
|
246
|
+
gap,
|
|
247
|
+
today: t,
|
|
248
|
+
words,
|
|
249
|
+
});
|
|
250
|
+
const open = (kind, item) => (onOpen ? () => onOpen({ kind, id: item.id, item }) : undefined);
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<div role="group" aria-label={label} className={cx("@container grid min-w-0 gap-3", className)}>
|
|
254
|
+
<p className="sr-only">{summary}</p>
|
|
255
|
+
<div role="region" aria-label={`${label}, scrolls sideways`} tabIndex={0} className="overflow-x-auto rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
|
256
|
+
<div className="relative w-full" style={{ minWidth, height }}>
|
|
257
|
+
{/* the axis */}
|
|
258
|
+
<span aria-hidden="true" className="absolute inset-x-0 h-px bg-foreground" style={{ top: AXIS - 4 }} />
|
|
259
|
+
{axis.weeks.map((d) => (
|
|
260
|
+
<span key={`w${d}`} aria-hidden="true" className="absolute h-[5px] w-px bg-foreground" style={{ left: pct(x(d)), top: AXIS - 9 }} />
|
|
261
|
+
))}
|
|
262
|
+
{axis.months.map((m) => (
|
|
263
|
+
<span key={`m${m.day}`} aria-hidden="true">
|
|
264
|
+
{m.tick ? <span className="absolute h-[17px] w-px bg-foreground" style={{ left: pct(x(m.day)), top: AXIS - 21 }} /> : null}
|
|
265
|
+
<span className="absolute top-0 whitespace-nowrap pl-1.5 font-mono text-xs uppercase tracking-[0.06em] text-muted-foreground" style={{ left: pct(x(m.day)) }}>
|
|
266
|
+
{m.label}
|
|
267
|
+
</span>
|
|
268
|
+
</span>
|
|
269
|
+
))}
|
|
270
|
+
|
|
271
|
+
{/* milestones */}
|
|
272
|
+
{mileItems.map((m) => {
|
|
273
|
+
const lane = mileLay.lanes[m.id];
|
|
274
|
+
const top = AXIS + 4 + lane * MILE_H;
|
|
275
|
+
const Mark = onOpen ? "button" : "span";
|
|
276
|
+
return (
|
|
277
|
+
<Mark
|
|
278
|
+
key={m.id}
|
|
279
|
+
{...(onOpen ? { type: "button", onClick: open("milestone", m.m), "aria-label": `${m.m.label}${m.m.done ? ", done" : ""}, ${dayName(m.m.day)}` } : { "aria-hidden": "true" })}
|
|
280
|
+
className={cx("absolute flex h-[18px] items-center gap-1.5 whitespace-nowrap rounded-sm bg-background pr-1 text-left font-mono text-xs outline-none", onOpen && "focus-visible:ring-2 focus-visible:ring-ring")}
|
|
281
|
+
style={m.side === "right" ? { left: `calc(${pct(m.px)} - 6px)`, top } : { right: `calc(${pct(100 - m.px)} - 6px)`, top, flexDirection: "row-reverse" }}
|
|
282
|
+
>
|
|
283
|
+
<span aria-hidden="true" className={cx("block size-[11px] shrink-0 rotate-45 border-[1.5px] border-foreground", m.m.done ? "bg-foreground" : "bg-background")} />
|
|
284
|
+
<span className="font-sans text-xs font-medium text-foreground">{m.m.label}</span>
|
|
285
|
+
<span className="text-muted-foreground">{shortDay(m.m.day)}</span>
|
|
286
|
+
</Mark>
|
|
287
|
+
);
|
|
288
|
+
})}
|
|
289
|
+
|
|
290
|
+
{/* where the chain sat before */}
|
|
291
|
+
{ghosts.map((g, i) => (
|
|
292
|
+
<span
|
|
293
|
+
key={`g${i}`}
|
|
294
|
+
aria-hidden="true"
|
|
295
|
+
title={words.early}
|
|
296
|
+
className="absolute border border-dashed border-muted-foreground"
|
|
297
|
+
style={{ left: pct(x(g.s)), width: `calc(${x(g.e + 1) - x(g.s)}% - 3px)`, top: bandTop - GHOST_H - 3, height: GHOST_H - 4 }}
|
|
298
|
+
/>
|
|
299
|
+
))}
|
|
300
|
+
|
|
301
|
+
{/* the segments */}
|
|
302
|
+
{segs.map((p, i) => {
|
|
303
|
+
const x0 = x(p.s);
|
|
304
|
+
const x1 = x(p.e + 1);
|
|
305
|
+
const Seg = onOpen ? "button" : "span";
|
|
306
|
+
const status = STATUS_WORDS[p.status] ?? p.status ?? "planned";
|
|
307
|
+
return (
|
|
308
|
+
<Seg
|
|
309
|
+
key={p.id}
|
|
310
|
+
{...(onOpen
|
|
311
|
+
? { type: "button", onClick: open("segment", p), "aria-label": `${p.label}, ${status}, ${shortDay(p.s)} to ${shortDay(p.e)}` }
|
|
312
|
+
: { "aria-hidden": "true" })}
|
|
313
|
+
title={`${p.label}, ${status}, ${shortDay(p.s)} to ${shortDay(p.e)}`}
|
|
314
|
+
className={cx(
|
|
315
|
+
"absolute flex min-w-[3px] flex-col justify-between overflow-hidden px-2.5 py-2 text-left outline-none",
|
|
316
|
+
TONE[p.status] ?? TONE.planned,
|
|
317
|
+
onOpen && "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
|
|
318
|
+
)}
|
|
319
|
+
style={{ left: pct(x0), width: `calc(${x1 - x0}% - 3px)`, top: bandTop, height: bandH }}
|
|
320
|
+
>
|
|
321
|
+
<span className="truncate text-sm font-semibold leading-tight">{p.label}</span>
|
|
322
|
+
{compact ? null : <span className="truncate font-mono text-xs leading-tight">{p.sub ?? i + 1}</span>}
|
|
323
|
+
</Seg>
|
|
324
|
+
);
|
|
325
|
+
})}
|
|
326
|
+
|
|
327
|
+
{/* the gap: slack in green, an overrun in red, hatched */}
|
|
328
|
+
{gap && gap.state !== "on" ? (
|
|
329
|
+
<span
|
|
330
|
+
aria-hidden="true"
|
|
331
|
+
className={cx("absolute border-2 border-current bg-background/70", GAP_TEXT[gap.state])}
|
|
332
|
+
style={{ ...HATCH, left: `calc(${x(gap.from)}% + 1px)`, width: `max(3px, calc(${x(gap.to + 1) - x(gap.from)}% - 3px))`, top: bandTop, height: bandH }}
|
|
333
|
+
/>
|
|
334
|
+
) : null}
|
|
335
|
+
{gap ? (
|
|
336
|
+
<span
|
|
337
|
+
aria-hidden="true"
|
|
338
|
+
className={cx("absolute whitespace-nowrap font-mono text-xs font-semibold", GAP_TEXT[gap.state])}
|
|
339
|
+
style={
|
|
340
|
+
gap.state === "on"
|
|
341
|
+
? { left: pct(x(gap.target + 1)), top: gapTop, transform: x(gap.target + 1) > 78 ? "translateX(-100%)" : undefined }
|
|
342
|
+
: x(gap.from) > 72
|
|
343
|
+
? { right: pct(100 - x(gap.to + 1)), top: gapTop }
|
|
344
|
+
: { left: pct(x(gap.from)), top: gapTop }
|
|
345
|
+
}
|
|
346
|
+
>
|
|
347
|
+
{gapWords(gap, words)}
|
|
348
|
+
</span>
|
|
349
|
+
) : null}
|
|
350
|
+
|
|
351
|
+
{/* gate posts */}
|
|
352
|
+
{gateItems.map(({ id, p, g, gx, side }) => {
|
|
353
|
+
const lane = gateLay.lanes[id];
|
|
354
|
+
const top = bandBottom + 8 + lane * GATE_H;
|
|
355
|
+
const stateTone = g.state === "overdue" ? "text-destructive" : "text-muted-foreground";
|
|
356
|
+
const Post = onOpen && g.total ? "button" : "span";
|
|
357
|
+
const said = `${p.label} gate, ${shortDay(g.day)}${g.total ? `, ${g.met} of ${g.total} criteria met` : ""}${g.state === "overdue" ? ", overdue" : ""}`;
|
|
358
|
+
return (
|
|
359
|
+
<span key={`gate-${id}`}>
|
|
360
|
+
<span aria-hidden="true" className="absolute w-px bg-foreground" style={{ left: `calc(${pct(gx)} - 3.5px)`, top: bandBottom, height: 8 + lane * GATE_H }} />
|
|
361
|
+
<span
|
|
362
|
+
aria-hidden="true"
|
|
363
|
+
className={cx("absolute size-[9px] border border-foreground", g.state === "met" ? "bg-success border-success" : "bg-background")}
|
|
364
|
+
style={{ left: `calc(${pct(gx)} - 8px)`, top: bandBottom - 4 }}
|
|
365
|
+
/>
|
|
366
|
+
<Post
|
|
367
|
+
{...(Post === "button" ? { type: "button", onClick: open("gate", p), "aria-label": said } : { "aria-hidden": "true" })}
|
|
368
|
+
className={cx(
|
|
369
|
+
"absolute grid gap-1 rounded-sm bg-background px-1 text-left outline-none",
|
|
370
|
+
Post === "button" && "focus-visible:ring-2 focus-visible:ring-ring",
|
|
371
|
+
side === "left" ? "justify-items-end" : "justify-items-start",
|
|
372
|
+
)}
|
|
373
|
+
style={side === "left" ? { right: `calc(${pct(100 - gx)} + 6px)`, top } : { left: `calc(${pct(gx)} + 3px)`, top }}
|
|
374
|
+
>
|
|
375
|
+
<span className={cx("font-mono text-xs leading-none", stateTone)}>
|
|
376
|
+
{shortDay(g.day)}
|
|
377
|
+
{g.total ? <span className="ml-1.5 tabular-nums">{g.met}/{g.total}</span> : null}
|
|
378
|
+
</span>
|
|
379
|
+
{g.total ? (
|
|
380
|
+
<span className="flex items-center gap-[3px]">
|
|
381
|
+
{g.pips.map((pip, k) => (
|
|
382
|
+
<span
|
|
383
|
+
key={pip.id ?? k}
|
|
384
|
+
title={`${pip.label ?? "Criterion"}${pip.met ? ", met" : ", not met"}`}
|
|
385
|
+
className={cx("block size-[9px] rounded-full border", pip.met ? "border-success bg-success" : "border-foreground bg-background")}
|
|
386
|
+
/>
|
|
387
|
+
))}
|
|
388
|
+
</span>
|
|
389
|
+
) : null}
|
|
390
|
+
</Post>
|
|
391
|
+
</span>
|
|
392
|
+
);
|
|
393
|
+
})}
|
|
394
|
+
|
|
395
|
+
{/* the target, the forecast, today */}
|
|
396
|
+
{tgt != null ? (
|
|
397
|
+
<span aria-hidden="true" className="absolute w-0.5 bg-foreground" style={{ left: `calc(${pct(x(tgt + 1))} - 2px)`, top: AXIS - 14, height: lineBottom("target") - (AXIS - 14) }} />
|
|
398
|
+
) : null}
|
|
399
|
+
{fc != null && gap?.state !== "on" && flags.some((f) => f.id === "forecast") ? (
|
|
400
|
+
<span
|
|
401
|
+
aria-hidden="true"
|
|
402
|
+
className={cx("absolute w-0 border-l-2 border-dashed", gap ? GAP_LINE[gap.state] : "border-foreground")}
|
|
403
|
+
style={{ left: `calc(${pct(x(fc + 1))} - 2px)`, top: AXIS - 14, height: lineBottom("forecast") - (AXIS - 14) }}
|
|
404
|
+
/>
|
|
405
|
+
) : null}
|
|
406
|
+
{tIn != null ? (
|
|
407
|
+
<span aria-hidden="true" className="absolute w-0.5 bg-primary" style={{ left: `calc(${pct(x(tIn))} - 1px)`, top: AXIS - 4, height: (compact ? bandBottom + 6 : lineBottom("today")) - (AXIS - 4) }} />
|
|
408
|
+
) : null}
|
|
409
|
+
{flagItems.map((f) => {
|
|
410
|
+
const lane = flagLay.lanes[f.id];
|
|
411
|
+
const top = flagTop + lane * FLAG_H;
|
|
412
|
+
const place = f.side === "left" ? { right: `calc(${pct(100 - f.fx)} + 6px)` } : { left: `calc(${pct(f.fx)} + 6px)` };
|
|
413
|
+
const tone = f.kind === "forecast" ? (gap ? GAP_TEXT[gap.state] : "text-foreground") : "text-foreground";
|
|
414
|
+
return f.kind === "today" ? (
|
|
415
|
+
<span
|
|
416
|
+
key={f.id}
|
|
417
|
+
aria-hidden="true"
|
|
418
|
+
className="absolute whitespace-nowrap bg-primary px-1.5 py-0.5 font-mono text-xs font-medium text-primary-foreground"
|
|
419
|
+
style={f.side === "left" ? { right: `calc(${pct(100 - f.fx)} - 1px)`, top } : { left: `calc(${pct(f.fx)} - 1px)`, top }}
|
|
420
|
+
>
|
|
421
|
+
{f.text}
|
|
422
|
+
</span>
|
|
423
|
+
) : (
|
|
424
|
+
<span key={f.id} aria-hidden="true" className={cx("absolute flex items-center gap-1 whitespace-nowrap bg-background px-1 font-mono text-xs font-medium", tone)} style={{ ...place, top }}>
|
|
425
|
+
<Icon name="flag" size={12} className="shrink-0" />
|
|
426
|
+
{f.text}
|
|
427
|
+
</span>
|
|
428
|
+
);
|
|
429
|
+
})}
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
{skipped ? <p className="text-xs text-muted-foreground">{words.undated(skipped)}</p> : null}
|
|
434
|
+
|
|
435
|
+
{/* under 36rem the same chain in words, because a scrolled picture hides its ends */}
|
|
436
|
+
<ol aria-hidden="true" className="grid min-w-0 gap-1.5 @xl:hidden">
|
|
437
|
+
{segs.map((p, i) => {
|
|
438
|
+
const g = gateOf(p.gate, p.e, t);
|
|
439
|
+
return (
|
|
440
|
+
<li key={p.id} className="flex min-w-0 items-baseline justify-between gap-3 border-t border-border pt-1.5 text-sm">
|
|
441
|
+
<span className="min-w-0 truncate font-medium">{i + 1}. {p.label}</span>
|
|
442
|
+
<span className="shrink-0 text-right font-mono text-xs tabular-nums text-muted-foreground">
|
|
443
|
+
{shortDay(p.s)} to {shortDay(p.e)}
|
|
444
|
+
{g?.total ? ` · ${g.met}/${g.total} met` : ""}
|
|
445
|
+
</span>
|
|
446
|
+
</li>
|
|
447
|
+
);
|
|
448
|
+
})}
|
|
449
|
+
{miles.map((m) => (
|
|
450
|
+
<li key={m.id} className="flex min-w-0 items-baseline justify-between gap-3 border-t border-border pt-1.5 text-sm">
|
|
451
|
+
<span className="min-w-0 truncate font-medium">{m.label}</span>
|
|
452
|
+
<span className="shrink-0 font-mono text-xs tabular-nums text-muted-foreground">{shortDay(m.day)}{m.done ? " · done" : ""}</span>
|
|
453
|
+
</li>
|
|
454
|
+
))}
|
|
455
|
+
{tgt != null || gap ? (
|
|
456
|
+
<li className="flex min-w-0 flex-col gap-0.5 border-t border-border pt-1.5 font-mono text-xs tabular-nums">
|
|
457
|
+
{tgt != null ? <span className="text-foreground">{words.target} {dayName(tgt)}</span> : null}
|
|
458
|
+
{fc != null && gap?.state !== "on" ? <span className={gap ? GAP_TEXT[gap.state] : "text-foreground"}>{words.forecast} {dayName(fc)}</span> : null}
|
|
459
|
+
{gap ? <span className={cx("font-semibold", GAP_TEXT[gap.state])}>{gapWords(gap, words)}</span> : null}
|
|
460
|
+
</li>
|
|
461
|
+
) : null}
|
|
462
|
+
</ol>
|
|
463
|
+
</div>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* The target against the forecast, drawn and nothing else: a dashed line
|
|
469
|
+
* for the target, a solid bar for the forecast, the gap between them
|
|
470
|
+
* hatched (green early, red late) and its size in words underneath. For a
|
|
471
|
+
* sentence to sit beside ("Public launch lands Tue 3 Nov").
|
|
472
|
+
*/
|
|
473
|
+
export function GapRuler({ target, forecast, count = "working", workweek, words: wordsIn, label, width = 380, className }) {
|
|
474
|
+
const words = { ...WORDS, ...wordsIn };
|
|
475
|
+
const gap = gapOf({ target, forecast, count, workweek });
|
|
476
|
+
if (!gap) return null;
|
|
477
|
+
const lo = Math.min(gap.target, gap.forecast);
|
|
478
|
+
const hi = Math.max(gap.target, gap.forecast);
|
|
479
|
+
const total = Math.max(hi - lo + 6, 14);
|
|
480
|
+
const r0 = lo - Math.floor((total - (hi - lo)) / 2);
|
|
481
|
+
const at = (d) => ((d - r0) / total) * 100;
|
|
482
|
+
const tx = at(gap.target);
|
|
483
|
+
const fx = at(gap.forecast);
|
|
484
|
+
const a = Math.min(tx, fx);
|
|
485
|
+
const b = Math.max(tx, fx);
|
|
486
|
+
const tone = GAP_TEXT[gap.state];
|
|
487
|
+
const earlierIsTarget = gap.target <= gap.forecast;
|
|
488
|
+
const said = label ?? gapWords(gap, words);
|
|
489
|
+
return (
|
|
490
|
+
<div
|
|
491
|
+
role="img"
|
|
492
|
+
aria-label={`${words.target} ${dayName(gap.target)}, ${words.forecast.toLowerCase()} ${dayName(gap.forecast)}: ${said}`}
|
|
493
|
+
className={cx("@container relative h-[92px] w-full min-w-0", className)}
|
|
494
|
+
style={{ maxWidth: width }}
|
|
495
|
+
>
|
|
496
|
+
<span aria-hidden="true" className="absolute inset-x-0 top-[46px] h-px bg-foreground" />
|
|
497
|
+
{gap.state !== "on" ? (
|
|
498
|
+
<span aria-hidden="true" className={cx("absolute top-[34px] h-6 border-y-2 border-current", tone)} style={{ ...HATCH, left: pct(a), width: pct(b - a) }} />
|
|
499
|
+
) : null}
|
|
500
|
+
<span aria-hidden="true" className="absolute top-6 h-11 w-0 border-l-[1.5px] border-dashed border-foreground" style={{ left: pct(tx) }} />
|
|
501
|
+
<span aria-hidden="true" className={cx("absolute top-5 h-[52px] w-[3px] -translate-x-px bg-current", tone)} style={{ left: pct(fx) }} />
|
|
502
|
+
<span aria-hidden="true" className={cx("absolute top-0 whitespace-nowrap font-mono text-xs leading-tight text-muted-foreground", earlierIsTarget ? "-translate-x-full pr-1.5 text-right" : "pl-1.5")} style={{ left: pct(tx) }}>
|
|
503
|
+
{words.target.toUpperCase()}
|
|
504
|
+
<br />
|
|
505
|
+
{shortDay(gap.target)}
|
|
506
|
+
</span>
|
|
507
|
+
<span aria-hidden="true" className={cx("absolute top-0 whitespace-nowrap font-mono text-xs font-semibold leading-tight", tone, earlierIsTarget ? "pl-1.5" : "-translate-x-full pr-1.5 text-right")} style={{ left: pct(fx) }}>
|
|
508
|
+
{words.forecast.toUpperCase()}
|
|
509
|
+
<br />
|
|
510
|
+
{shortDay(gap.forecast)}
|
|
511
|
+
</span>
|
|
512
|
+
<span aria-hidden="true" className={cx("absolute top-[72px] whitespace-nowrap font-mono text-xs font-semibold", tone)} style={{ left: pct((a + b) / 2), transform: "translateX(-50%)" }}>
|
|
513
|
+
{said}
|
|
514
|
+
</span>
|
|
515
|
+
</div>
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* The chain small: one row of bars with the gap hatched, the target in ink
|
|
521
|
+
* and today in the accent. For a row in a list; put the numbers in words
|
|
522
|
+
* beside it.
|
|
523
|
+
*/
|
|
524
|
+
export function ScheduleStrip({ segments, target, forecast, today, count = "working", workweek, height = 16, label = "Schedule", className }) {
|
|
525
|
+
const { segs, tgt, fc, t, gap } = readProps({ segments, target, forecast, today, count, workweek });
|
|
526
|
+
const days = [...segs.flatMap((p) => [p.s, p.e + 1]), tgt != null ? tgt + 1 : null, fc != null ? fc + 1 : null].filter((d) => d != null);
|
|
527
|
+
const frame = frameOf({ days, padBefore: 0, padAfter: 1 });
|
|
528
|
+
if (!frame || (!segs.length && tgt == null)) return <div aria-hidden="true" className={cx("w-full border-b border-border", className)} style={{ height: height + 6 }} />;
|
|
529
|
+
const { x, r0, r1 } = frame;
|
|
530
|
+
const summary = segs.map((p) => `${p.label} ${STATUS_WORDS[p.status] ?? p.status ?? "planned"}`).join(", ");
|
|
531
|
+
return (
|
|
532
|
+
<div
|
|
533
|
+
role="img"
|
|
534
|
+
aria-label={`${label}: ${summary || "no dated items"}${gap ? `; ${gapWords(gap, { ...WORDS, over: (n) => `${n} days over`, slack: (n) => `${n} days of slack` })}` : ""}`}
|
|
535
|
+
className={cx("relative w-full min-w-0 border-b border-border", className)}
|
|
536
|
+
style={{ height: height + 6 }}
|
|
537
|
+
>
|
|
538
|
+
{segs.map((p) => (
|
|
539
|
+
<span key={p.id} aria-hidden="true" className={cx("absolute top-0", STRIP[p.status] ?? STRIP.planned)} style={{ left: pct(x(p.s)), width: `calc(${x(p.e + 1) - x(p.s)}% - 2px)`, height }} />
|
|
540
|
+
))}
|
|
541
|
+
{gap && gap.state !== "on" ? (
|
|
542
|
+
<span aria-hidden="true" className={cx("absolute top-0 border border-current bg-background/70", GAP_TEXT[gap.state])} style={{ ...HATCH_FINE, left: pct(x(gap.from)), width: `max(2px, calc(${x(gap.to + 1) - x(gap.from)}% - 1px))`, height }} />
|
|
543
|
+
) : null}
|
|
544
|
+
{tgt != null ? <span aria-hidden="true" className="absolute -top-1 w-0.5 bg-foreground" style={{ left: `calc(${pct(x(tgt + 1))} - 1px)`, height: height + 10 }} /> : null}
|
|
545
|
+
{t != null && t >= r0 && t <= r1 ? <span aria-hidden="true" className="absolute w-0.5 bg-primary" style={{ left: `calc(${pct(x(t))} - 1px)`, top: -5, height: height + 11 }} /> : null}
|
|
546
|
+
</div>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# SourceReceipt
|
|
2
|
+
|
|
3
|
+
Where one value came from, drawn as a clipping:
|
|
4
|
+
|
|
5
|
+
- the page's or the mail's title, with the trust mark's shape beside it;
|
|
6
|
+
- its address (linked) or the mailbox, and the date it was read;
|
|
7
|
+
- the words on it, with the **deciding words underlined** wherever they
|
|
8
|
+
appear;
|
|
9
|
+
- a trust word ("their own site", "a 2023 directory");
|
|
10
|
+
- **Open the source**.
|
|
11
|
+
|
|
12
|
+
A member reads the claim and its proof in one look, and decides how far
|
|
13
|
+
to trust it.
|
|
14
|
+
|
|
15
|
+
It is not a second receipt. The clipping is `agent-column`'s (Data
|
|
16
|
+
Enrichment's "the receipt"); this block wraps it for one source standing
|
|
17
|
+
on its own: a CRM's found title, a note's proof margin, a newsletter's
|
|
18
|
+
source line. Inside a column of answers, use `agent-column`'s `Receipt`.
|
|
19
|
+
|
|
20
|
+
```jsx
|
|
21
|
+
import { SourceReceipt } from "../components/blocks/source-receipt"; // a Makersclaw app ("./blocks/source-receipt" from components/)
|
|
22
|
+
import { SourceReceipt } from "@/components/makersclaw/blocks/source-receipt"; // a shadcn project
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
A client component. It requires `agent-column` (and through it
|
|
26
|
+
`agent-marks`), laid with it.
|
|
27
|
+
|
|
28
|
+
## Which hook feeds it
|
|
29
|
+
|
|
30
|
+
None. The source is app data the agent wrote beside the value: its
|
|
31
|
+
address, its title, the words it read, the words that decided. Keep
|
|
32
|
+
them in the record (a `source` column, a `sources` table) and pass them
|
|
33
|
+
in.
|
|
34
|
+
|
|
35
|
+
## Props
|
|
36
|
+
|
|
37
|
+
- `title` — the page's or the mail's title ("Careers · Klarwerk").
|
|
38
|
+
- `url` — the source's address. The clipping shows it short
|
|
39
|
+
("klarwerk.de/careers") and links it.
|
|
40
|
+
- `site` — where, when there is no `url`: a mailbox ("Sofia Reyes
|
|
41
|
+
<sofia@crumb.co>") or a site's name. From `url` when left out.
|
|
42
|
+
- `kind` — `site` (default) or `mail`: the icon before the trust word.
|
|
43
|
+
- `date` — when it was read or sent, in words ("read 22 Sep").
|
|
44
|
+
- `trust` — a word for how far to trust it ("their own site", "her
|
|
45
|
+
signature", "a 2023 directory").
|
|
46
|
+
- `sure` — `sure` · `likely` · `guess`: the trust mark's shape beside
|
|
47
|
+
the title (filled, half, dashed).
|
|
48
|
+
- `excerpt` — the words on it.
|
|
49
|
+
- `marks` — the deciding words, `string[]`. Every occurrence of each is
|
|
50
|
+
underlined, whatever its case. Without `marks`, an excerpt's own
|
|
51
|
+
`[[…]]` (the agent's marking) are underlined instead.
|
|
52
|
+
- `onOpen()` — opens the source your way. Without it, a `url` opens in a
|
|
53
|
+
new tab.
|
|
54
|
+
- `openLabel` — default "Open the source".
|
|
55
|
+
- `action` — a node at the head's end: a "Pick 70" when two sources
|
|
56
|
+
disagree.
|
|
57
|
+
- `picked` — drawn as the one chosen.
|
|
58
|
+
- `className` — layout around it. Never colour.
|
|
59
|
+
|
|
60
|
+
`markExcerpt(excerpt, marks)` is exported: the excerpt with `[[…]]`
|
|
61
|
+
around every deciding word, as the clipping reads it.
|
|
62
|
+
|
|
63
|
+
## Who may do what
|
|
64
|
+
|
|
65
|
+
Anyone who can see the value can see its source. The receipt has no
|
|
66
|
+
writes; a Pick is your own `action`, and your own action's rules hold.
|
|
67
|
+
|
|
68
|
+
## Never
|
|
69
|
+
|
|
70
|
+
- Never build the excerpt from HTML or inject it: it is drawn as text,
|
|
71
|
+
and the underlines are elements, never markup in a string.
|
|
72
|
+
- Never show a value as found with no receipt to open.
|
|
73
|
+
- Never colour the trust alone: the mark's shape and the word say it.
|
|
74
|
+
|
|
75
|
+
## Example
|
|
76
|
+
|
|
77
|
+
The source of a contact's job title, under the field.
|
|
78
|
+
|
|
79
|
+
```jsx
|
|
80
|
+
import { SourceReceipt } from "./blocks/source-receipt";
|
|
81
|
+
|
|
82
|
+
export function TitleSource({ person }) {
|
|
83
|
+
const s = person.title_source;
|
|
84
|
+
if (!s) return null;
|
|
85
|
+
return (
|
|
86
|
+
<SourceReceipt
|
|
87
|
+
title={s.title}
|
|
88
|
+
url={s.url}
|
|
89
|
+
date={`read ${s.read_on}`}
|
|
90
|
+
trust={s.trust}
|
|
91
|
+
sure={s.sure}
|
|
92
|
+
excerpt={s.words}
|
|
93
|
+
marks={[person.job_title]}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "source-receipt",
|
|
3
|
+
"title": "SourceReceipt",
|
|
4
|
+
"description": "Where one value came from, as a clipping: the page or the mail and its title, the address, the date it was read, a trust word (\"their own site\", \"a 2023 directory\") and the trust mark's shape, the words on it with the deciding ones underlined wherever they appear (case-insensitive, never HTML), and Open the source. A thin wrapper over agent-column's clipping (Data Enrichment's receipt) for a value that stands on its own, so every app draws a source the same way.",
|
|
5
|
+
"useWhen": "A value the agent found should show its proof beside it: a contact's title from an email signature, a company's size from its site, a figure in a note or a newsletter with its source in the margin, two sources side by side for a person to pick. For a whole column of answers across records use agent-column; for a value proposed but not kept yet, put the receipt under a suggestion.",
|
|
6
|
+
"tags": [
|
|
7
|
+
"source",
|
|
8
|
+
"citation",
|
|
9
|
+
"receipt",
|
|
10
|
+
"provenance",
|
|
11
|
+
"evidence",
|
|
12
|
+
"quote",
|
|
13
|
+
"clipping",
|
|
14
|
+
"trust",
|
|
15
|
+
"proof",
|
|
16
|
+
"reference",
|
|
17
|
+
"found"
|
|
18
|
+
],
|
|
19
|
+
"exports": [
|
|
20
|
+
"SourceReceipt",
|
|
21
|
+
"markExcerpt"
|
|
22
|
+
],
|
|
23
|
+
"files": [
|
|
24
|
+
"index.js",
|
|
25
|
+
"source-receipt.jsx"
|
|
26
|
+
],
|
|
27
|
+
"requires": [
|
|
28
|
+
"agent-column"
|
|
29
|
+
]
|
|
30
|
+
}
|