makersclaw 0.7.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 +17 -4
- 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 +4 -4
- 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 +5 -5
- package/r/chat-pane.json +84 -0
- package/r/chat.json +3 -3
- 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 +18 -6
- 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 +3 -3
- package/r/flow-editor.json +79 -0
- package/r/footer.json +3 -3
- 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 +4 -4
- 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 +4 -4
- package/r/pace-bar.json +69 -0
- package/r/page-builder.json +3 -3
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +3 -3
- 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 +4089 -1510
- 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 +3 -3
- package/r/signature-pad.json +2 -2
- package/r/sketch-canvas.json +3 -3
- 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 +4 -4
- 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/annotate/README.md +14 -11
- 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/chart/cartesian.jsx +1 -1
- package/registry/blocks/chart/core.jsx +33 -23
- package/registry/blocks/chart/pie.jsx +1 -1
- package/registry/blocks/chat/README.md +4 -1
- package/registry/blocks/chat/demo.jsx +2 -2
- 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-canvas/README.md +15 -8
- 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/footer/README.md +3 -2
- 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/mind-map/README.md +4 -2
- 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/org-chart/README.md +4 -2
- 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/page-builder/README.md +1 -1
- package/registry/blocks/page-builder/demo.jsx +3 -3
- package/registry/blocks/pdf-viewer/README.md +4 -1
- 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/sidebar/README.md +23 -13
- package/registry/blocks/sketch-canvas/README.md +15 -9
- 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/whiteboard/README.md +20 -14
- 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/cli.mjs +2 -2
- package/src/install.mjs +100 -22
- package/src/registry.mjs +37 -3
- package/templates/next/browser/lib/data.js +6 -1
- package/templates/next/files/README.md +909 -26
- package/templates/next/files/app/shell.jsx +54 -37
- 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 +1043 -12
- 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 +3 -1
- package/templates/next/files/components/ui/layout.jsx +382 -0
- 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 +244 -5
- package/templates/next/files/lib/door.js +92 -0
- package/templates/next/files/lib/hooks.js +1694 -6
- package/templates/next/files/lib/platform.js +611 -14
- package/templates/next/template.json +22 -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
|
+
}
|
|
@@ -57,8 +57,11 @@ true) is where it starts. The icon rail shows every group unfolded.
|
|
|
57
57
|
- `shortcut` — Cmd/Ctrl + this key toggles it; default `"b"`, `null` for none.
|
|
58
58
|
- `variant` — `"inset"` draws the page as a card on the frame's ground.
|
|
59
59
|
Default: the Sidebar's own `variant`, so you rarely pass it here.
|
|
60
|
-
- `className` — the frame's
|
|
61
|
-
|
|
60
|
+
- `className` — the frame's edges, and its height from its parent: in a
|
|
61
|
+
shell it sits in the kit's `AppLayout contentMode="fill"` with
|
|
62
|
+
`min-h-0 flex-1` (the Example). The kit's `AppLayout` owns the
|
|
63
|
+
window's height; `min-h-dvh` or `h-dvh` here is refused by `npm run
|
|
64
|
+
check`. Never colour.
|
|
62
65
|
|
|
63
66
|
### Sidebar
|
|
64
67
|
|
|
@@ -138,6 +141,7 @@ The header button: opens the sheet on a phone, collapses to icons from
|
|
|
138
141
|
// app/shell.jsx — the app's shell, the surfaces as the sidebar's items
|
|
139
142
|
"use client";
|
|
140
143
|
import { usePathname } from "next/navigation";
|
|
144
|
+
import { AppLayout } from "../components/ui";
|
|
141
145
|
import { Sidebar, SidebarLayout, SidebarTrigger } from "../components/blocks/sidebar";
|
|
142
146
|
|
|
143
147
|
const ICONS = { inbox: "inbox", deals: "dollar", contacts: "users", reports: "chart" };
|
|
@@ -153,18 +157,24 @@ export default function Shell({ name, surfaces, children }) {
|
|
|
153
157
|
active: s.key === activeKey,
|
|
154
158
|
badge: s.built ? undefined : "planned",
|
|
155
159
|
}));
|
|
160
|
+
// The outer AppLayout owns the window; the inner one (it fills its
|
|
161
|
+
// slot) is the content that scrolls, back to its top on a new route.
|
|
156
162
|
return (
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
<AppLayout contentMode="fill">
|
|
164
|
+
<SidebarLayout
|
|
165
|
+
storageKey={`${name}.sidebar`}
|
|
166
|
+
className="min-h-0 flex-1"
|
|
167
|
+
sidebar={<Sidebar title={name} items={items} label={`${name} surfaces`} />}
|
|
168
|
+
>
|
|
169
|
+
<header className="flex min-h-14 items-center gap-2 border-b border-border px-2 md:hidden">
|
|
170
|
+
<SidebarTrigger />
|
|
171
|
+
<span className="font-semibold">{name}</span>
|
|
172
|
+
</header>
|
|
173
|
+
<AppLayout contentClassName="p-4 sm:p-6" scrollKey={pathname}>
|
|
174
|
+
{children}
|
|
175
|
+
</AppLayout>
|
|
176
|
+
</SidebarLayout>
|
|
177
|
+
</AppLayout>
|
|
168
178
|
);
|
|
169
179
|
}
|
|
170
180
|
```
|
|
@@ -130,8 +130,11 @@ A sketch is plain JSON. Store it as it comes out of `onChange` / `onSave`
|
|
|
130
130
|
- `onReady(handle)` — once, when the canvas has mounted.
|
|
131
131
|
- `label` — the accessible name of the canvas region (default "Sketch").
|
|
132
132
|
- `className` — layout: width, margin, height. The default height is
|
|
133
|
-
`h-[32rem]`; a height class of yours (`h-
|
|
134
|
-
|
|
133
|
+
`h-[32rem]`; a height class of yours (`h-full`, `min-h-…`, `min-h-0
|
|
134
|
+
flex-1`) replaces it. A canvas that is the whole surface sits in an
|
|
135
|
+
`AppLayout contentMode="fill"` from the kit with `min-h-0 flex-1` (the
|
|
136
|
+
Example): never `h-dvh` or a `vh` height, which `npm run check`
|
|
137
|
+
refuses. Never colour.
|
|
135
138
|
|
|
136
139
|
### The handle
|
|
137
140
|
|
|
@@ -220,6 +223,7 @@ A whiteboard that saves itself:
|
|
|
220
223
|
import { useSearchParams } from "next/navigation";
|
|
221
224
|
import { useRow } from "../lib/data";
|
|
222
225
|
import { updateRow } from "../lib/actions";
|
|
226
|
+
import { AppLayout } from "../components/ui";
|
|
223
227
|
import { SketchCanvas } from "../components/blocks/sketch-canvas";
|
|
224
228
|
|
|
225
229
|
export default function Board() {
|
|
@@ -227,13 +231,15 @@ export default function Board() {
|
|
|
227
231
|
const { row: board, loading } = useRow({ source: "boards", id });
|
|
228
232
|
if (loading || !board) return null;
|
|
229
233
|
return (
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
234
|
+
<AppLayout contentMode="fill">
|
|
235
|
+
<SketchCanvas
|
|
236
|
+
key={board.id}
|
|
237
|
+
label={board.title}
|
|
238
|
+
defaultValue={board.sketch} // jsonb, may be null; read once per board
|
|
239
|
+
onSave={(sketch) => updateRow({ source: "boards", id: board.id, patch: { sketch } })}
|
|
240
|
+
className="min-h-0 flex-1 rounded-none border-0"
|
|
241
|
+
/>
|
|
242
|
+
</AppLayout>
|
|
237
243
|
);
|
|
238
244
|
}
|
|
239
245
|
```
|